Typed Arrays in Practice: Real-World Applications
You've learned the fundamentals and mastered all 9 Typed Array types. Now let's build something real.
In this guide, we'll create three practical applications that use Typed Arrays to solve real problems:
- PNG File Reader - Extract image dimensions and metadata
- Simple Audio Generator - Create a playable tone
- Image Grayscale Filter - Convert images to black and white
These aren't complex—they're focused examples that demonstrate core patterns you'll use in any binary data project.
What You Need to Know First
Required reading:
- Typed Arrays Fundamentals - Views, buffers, basic operations
- The Complete Typed Array Family - When to use each type
You should be comfortable with:
- Creating Typed Arrays from buffers
- Reading and writing binary data
- The difference between Uint8Array, Int16Array, Float32Array, etc.
- Basic file I/O with the File API
What We'll Cover in This Article
By the end of this guide, you'll know how to:
- Read binary file formats (PNG example)
- Handle different byte orders (endianness)
- Write binary files (WAV audio example)
- Manipulate pixel data with Canvas
- Apply the patterns to your own projects
What We'll Explain Along the Way
We'll cover these concepts through working examples:
- File signatures - How to identify file types
- Chunk-based formats - Reading structured binary data
- Endianness - Big-endian vs little-endian
- Pixel manipulation - Direct image processing
- Audio samples - How digital audio works
Project 1: PNG File Info Reader
Let's start simple: reading basic information from a PNG file. Every PNG starts with a signature and header that tell us the image dimensions.
PNG Structure (Simplified)
[Bytes 0-7]: PNG Signature (always the same 8 bytes)
[Bytes 8-11]: "IHDR" chunk type
[Bytes 12-23]: Image header data (width, height, etc.)
Complete Implementation
/**
* Reads basic PNG file information
*/
class PNGReader {
private data: Uint8Array;
// PNG files always start with these exact bytes
private static readonly SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10];
constructor(arrayBuffer: ArrayBuffer) {
this.data = new Uint8Array(arrayBuffer);
}
/**
* Check if this is a valid PNG file
*/
isPNG(): boolean {
if (this.data.length < 8) return false;
for (let i = 0; i < 8; i++) {
if (this.data[i] !== PNGReader.SIGNATURE[i]) {
return false;
}
}
return true;
}
/**
* Read image dimensions
* PNG stores width and height as 32-bit big-endian integers
*/
getDimensions(): { width: number; height: number } | null {
if (!this.isPNG()) return null;
if (this.data.length < 24) return null;
// PNG uses big-endian byte order
// We need to read 4 bytes and combine them correctly
const width = this.readBigEndianUint32(16); // Width at byte 16
const height = this.readBigEndianUint32(20); // Height at byte 20
return { width, height };
}
/**
* Helper: Read a 32-bit big-endian number
* PNG uses big-endian (most significant byte first)
*/
private readBigEndianUint32(offset: number): number {
return (
(this.data[offset] << 24) | // Byte 0 (most significant)
(this.data[offset + 1] << 16) | // Byte 1
(this.data[offset + 2] << 8) | // Byte 2
this.data[offset + 3] // Byte 3 (least significant)
);
}
/**
* Get color type information
*/
getColorType(): string | null {
if (!this.isPNG()) return null;
if (this.data.length < 25) return null;
const colorType = this.data[25]; // Color type at byte 25
const types: Record<number, string> = {
0: "Grayscale",
2: "RGB",
3: "Indexed Color",
4: "Grayscale with Alpha",
6: "RGBA",
};
return types[colorType] || "Unknown";
}
}
Using the PNG Reader
/**
* Example: Read PNG file info
*/
async function readPNGInfo() {
// Create file input
const input = document.createElement("input");
input.type = "file";
input.accept = "image/png";
input.addEventListener("change", async (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (!file) return;
console.log("Reading:", file.name);
// Read file as ArrayBuffer
const buffer = await file.arrayBuffer();
// Parse PNG
const reader = new PNGReader(buffer);
if (!reader.isPNG()) {
console.error("Not a PNG file!");
return;
}
const dims = reader.getDimensions();
const colorType = reader.getColorType();
console.log("✓ Valid PNG file");
console.log("Dimensions:", `${dims?.width} × ${dims?.height}`);
console.log("Color type:", colorType);
console.log("File size:", (file.size / 1024).toFixed(2), "KB");
});
input.click();
}
// Run it
// readPNGInfo();
Example output:
Reading: photo.png
✓ Valid PNG file
Dimensions: 1920 × 1080
Color type: RGBA
File size: 245.67 KB
Key Concepts
1. File Signatures (Magic Numbers)
Every file type has a unique signature:
// Common signatures (first few bytes)
const PNG = [137, 80, 78, 71]; // PNG
const JPEG = [255, 216, 255]; // JPEG
const GIF = [71, 73, 70]; // GIF
const PDF = [37, 80, 68, 70]; // PDF
// Always check signature before parsing!
2. Endianness
PNG uses big-endian (most significant byte first):
// Number: 1920 (0x00000780 in hex)
// Big-endian (PNG): [00, 00, 07, 80]
// Read left to right: (0×16³) + (0×16²) + (7×16¹) + (8×16⁰) = 1920
// Little-endian (most computers): [80, 07, 00, 00]
// Read right to left: same result
// That's why we need special reading logic for PNG
Visual comparison:
Project 2: Simple Audio Tone Generator
Now let's create something from scratch: a WAV audio file that plays a tone.
WAV File Structure (Simplified)
[Bytes 0-43]: WAV Header (44 bytes total)
[Bytes 44+]: Audio samples (Int16Array)
Complete Implementation
/**
* Generates a simple WAV audio file
*/
class AudioGenerator {
private sampleRate = 44100; // CD quality (44.1 kHz)
/**
* Generate a sine wave tone
*/
generateTone(frequency: number, duration: number): Int16Array {
const numSamples = Math.floor(this.sampleRate * duration);
const samples = new Int16Array(numSamples);
// Generate sine wave
// Audio samples range from -32768 to 32767
for (let i = 0; i < numSamples; i++) {
const time = i / this.sampleRate;
const value = Math.sin(2 * Math.PI * frequency * time);
samples[i] = Math.round(value * 16000); // 16000 = 50% volume
}
return samples;
}
/**
* Create a WAV file from audio samples
*/
createWAV(samples: Int16Array): ArrayBuffer {
const dataSize = samples.length * 2; // 2 bytes per sample
const fileSize = 44 + dataSize;
const buffer = new ArrayBuffer(fileSize);
const view = new DataView(buffer);
// Helper to write strings
const writeString = (offset: number, str: string) => {
for (let i = 0; i < str.length; i++) {
view.setUint8(offset + i, str.charCodeAt(i));
}
};
// WAV Header (44 bytes)
writeString(0, "RIFF"); // Bytes 0-3
view.setUint32(4, fileSize - 8, true); // Bytes 4-7 (file size - 8)
writeString(8, "WAVE"); // Bytes 8-11
writeString(12, "fmt "); // Bytes 12-15
view.setUint32(16, 16, true); // Bytes 16-19 (format size)
view.setUint16(20, 1, true); // Bytes 20-21 (PCM = 1)
view.setUint16(22, 1, true); // Bytes 22-23 (mono = 1)
view.setUint32(24, this.sampleRate, true); // Bytes 24-27 (sample rate)
view.setUint32(28, this.sampleRate * 2, true); // Bytes 28-31 (byte rate)
view.setUint16(32, 2, true); // Bytes 32-33 (block align)
view.setUint16(34, 16, true); // Bytes 34-35 (bits per sample)
writeString(36, "data"); // Bytes 36-39
view.setUint32(40, dataSize, true); // Bytes 40-43 (data size)
// Write audio samples (44+)
for (let i = 0; i < samples.length; i++) {
view.setInt16(44 + i * 2, samples[i], true); // little-endian
}
return buffer;
}
/**
* Save WAV file
*/
save(samples: Int16Array, filename: string) {
const wav = this.createWAV(samples);
const blob = new Blob([wav], { type: "audio/wav" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = filename;
link.click();
URL.revokeObjectURL(url);
}
}
Using the Audio Generator
/**
* Example: Generate a 440 Hz tone (musical note A)
*/
function generateTone() {
const generator = new AudioGenerator();
console.log("Generating 440 Hz tone for 2 seconds...");
// Generate samples
const samples = generator.generateTone(440, 2);
console.log("Generated", samples.length, "samples");
// Save to file
generator.save(samples, "tone-440hz.wav");
console.log("✓ WAV file saved!");
// You can now open and play the file!
}
// generateTone();
Key Concepts
1. Audio Samples
Digital audio is just numbers representing speaker position:
// Int16Array range: -32768 to 32767
32767 ┐ Speaker pushed out (loudest)
0 ├ Speaker at rest (silence)
-32768 ┘ Speaker pulled in (loudest)
// A sine wave oscillates between these extremes
// More samples per second = higher quality
2. Sample Rate
// CD quality: 44,100 samples per second
const sampleRate = 44100;
// 1 second of audio = 44,100 samples
const oneSec = new Int16Array(44100);
// 0.5 seconds = 22,050 samples
const halfSec = new Int16Array(22050);
3. Why Int16Array for Audio
// CD-quality audio uses 16-bit signed integers
// Range: -32,768 to 32,767
// This gives 65,536 different levels
// 8-bit (Uint8Array): Only 256 levels - very noisy
// 16-bit (Int16Array): 65,536 levels - CD quality ✓
// 32-bit: Better, but 2× the file size for minimal gain
Project 3: Image Grayscale Filter
Let's manipulate images directly using Canvas and Uint8ClampedArray.
Understanding Canvas Pixel Data
// Canvas pixels are stored as RGBA (4 bytes per pixel)
// [R, G, B, A, R, G, B, A, R, G, B, A, ...]
// Pixel 0 Pixel 1 Pixel 2
const imageData = ctx.getImageData(0, 0, width, height);
console.log(imageData.data); // Uint8ClampedArray
Complete Implementation
/**
* Applies grayscale filter to images
*/
class GrayscaleFilter {
private canvas: HTMLCanvasElement;
private ctx: CanvasRenderingContext2D;
constructor(canvasId: string) {
this.canvas = document.getElementById(canvasId) as HTMLCanvasElement;
const ctx = this.canvas.getContext("2d");
if (!ctx) throw new Error("Could not get 2D context");
this.ctx = ctx;
}
/**
* Load image onto canvas
*/
async loadImage(file: File): Promise<void> {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
// Resize canvas to image size
this.canvas.width = img.width;
this.canvas.height = img.height;
// Draw image
this.ctx.drawImage(img, 0, 0);
URL.revokeObjectURL(img.src);
resolve();
};
img.onerror = reject;
img.src = URL.createObjectURL(file);
});
}
/**
* Apply grayscale filter
*/
applyGrayscale(): void {
// Get pixel data
const imageData = this.ctx.getImageData(
0,
0,
this.canvas.width,
this.canvas.height
);
const pixels = imageData.data; // Uint8ClampedArray
// Process each pixel
for (let i = 0; i < pixels.length; i += 4) {
const r = pixels[i];
const g = pixels[i + 1];
const b = pixels[i + 2];
// pixels[i + 3] is alpha (don't change)
// Calculate grayscale using luminosity formula
// Human eye is more sensitive to green
const gray = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
// Set all RGB channels to same value
pixels[i] = gray; // R
pixels[i + 1] = gray; // G
pixels[i + 2] = gray; // B
}
// Put modified pixels back
this.ctx.putImageData(imageData, 0, 0);
}
/**
* Brighten image
*/
adjustBrightness(amount: number): void {
const imageData = this.ctx.getImageData(
0,
0,
this.canvas.width,
this.canvas.height
);
const pixels = imageData.data;
// Add amount to each color channel
// Uint8ClampedArray automatically clamps to 0-255
for (let i = 0; i < pixels.length; i += 4) {
pixels[i] += amount; // R
pixels[i + 1] += amount; // G
pixels[i + 2] += amount; // B
}
this.ctx.putImageData(imageData, 0, 0);
}
/**
* Reset to original
*/
reset(originalData: ImageData): void {
this.ctx.putImageData(originalData, 0, 0);
}
}
Using the Filter
/**
* Simple image filter app
*/
async function filterApp() {
const filter = new GrayscaleFilter("canvas");
let original: ImageData | null = null;
// File input
const input = document.createElement("input");
input.type = "file";
input.accept = "image/*";
input.addEventListener("change", async (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (!file) return;
console.log("Loading:", file.name);
await filter.loadImage(file);
// Save original
const canvas = document.getElementById("canvas") as HTMLCanvasElement;
const ctx = canvas.getContext("2d")!;
original = ctx.getImageData(0, 0, canvas.width, canvas.height);
console.log("✓ Image loaded");
console.log("Dimensions:", canvas.width, "×", canvas.height);
});
// Grayscale button
document.getElementById("btnGrayscale")?.addEventListener("click", () => {
filter.applyGrayscale();
console.log("✓ Grayscale applied");
});
// Brighten button
document.getElementById("btnBrighten")?.addEventListener("click", () => {
filter.adjustBrightness(20);
console.log("✓ Brightness increased");
});
// Reset button
document.getElementById("btnReset")?.addEventListener("click", () => {
if (original) {
filter.reset(original);
console.log("✓ Reset to original");
}
});
input.click();
}
// HTML needed:
// <canvas id="canvas"></canvas>
// <button id="btnGrayscale">Grayscale</button>
// <button id="btnBrighten">Brighten</button>
// <button id="btnReset">Reset</button>
// filterApp();
Key Concepts
1. Why Uint8ClampedArray
Canvas uses Uint8ClampedArray because it automatically prevents errors:
// Without clamping (Uint8Array)
const normal = new Uint8Array([200]);
normal[0] += 100; // 300 → wraps to 44 (WRONG!)
// With clamping (Uint8ClampedArray)
const clamped = new Uint8ClampedArray([200]);
clamped[0] += 100; // 300 → clamps to 255 (CORRECT!)
// This prevents dark pixels when brightening images
2. Pixel Indexing
// Get pixel at coordinates (x, y)
function getPixelIndex(x: number, y: number, width: number): number {
return (y * width + x) * 4; // × 4 for RGBA
}
// Example: Pixel at (10, 5) in 100-wide image
const index = getPixelIndex(10, 5, 100);
// = (5 * 100 + 10) * 4
// = 510 * 4
// = 2040
const r = pixels[2040]; // Red
const g = pixels[2041]; // Green
const b = pixels[2042]; // Blue
const a = pixels[2043]; // Alpha
3. Processing Pattern
// Universal filter pattern
for (let i = 0; i < pixels.length; i += 4) {
// Read current pixel
const r = pixels[i];
const g = pixels[i + 1];
const b = pixels[i + 2];
const a = pixels[i + 3];
// Apply transformation
const [newR, newG, newB] = transform(r, g, b);
// Write back
pixels[i] = newR;
pixels[i + 1] = newG;
pixels[i + 2] = newB;
// Usually keep alpha unchanged
}
Common Patterns You've Learned
Pattern 1: Reading Binary Files
// 1. Get file as ArrayBuffer
const buffer = await file.arrayBuffer();
// 2. Create Uint8Array view
const bytes = new Uint8Array(buffer);
// 3. Check signature
if (bytes[0] !== EXPECTED_SIGNATURE) {
throw new Error("Invalid file");
}
// 4. Read data
const value = readValue(bytes, offset);
Pattern 2: Writing Binary Files
// 1. Calculate size
const size = HEADER_SIZE + dataSize;
// 2. Create buffer
const buffer = new ArrayBuffer(size);
// 3. Create views
const view = new DataView(buffer);
const bytes = new Uint8Array(buffer);
// 4. Write data
view.setUint32(0, value, true);
// 5. Create blob and download
const blob = new Blob([buffer]);
const url = URL.createObjectURL(blob);
Pattern 3: Processing Pixel Data
// 1. Get image data
const imageData = ctx.getImageData(0, 0, width, height);
const pixels = imageData.data; // Uint8ClampedArray
// 2. Process each pixel
for (let i = 0; i < pixels.length; i += 4) {
// Transform R, G, B
pixels[i] = transform(pixels[i]);
pixels[i + 1] = transform(pixels[i + 1]);
pixels[i + 2] = transform(pixels[i + 2]);
}
// 3. Put back
ctx.putImageData(imageData, 0, 0);
Summary: What You've Learned
You've built three working applications:
✓ PNG Reader - Parse binary files, handle endianness ✓ Audio Generator - Create binary files, work with Int16Array ✓ Image Filter - Manipulate pixels with Uint8ClampedArray
Core skills mastered:
✓ Reading binary file formats ✓ Handling big-endian and little-endian data ✓ Writing structured binary data ✓ Working with Canvas pixel data ✓ Choosing the right Typed Array type
Patterns you can now apply:
✓ File signature verification ✓ Binary file parsing ✓ Binary file generation ✓ Image processing ✓ Audio sample manipulation
Quick Reference
Reading files:
const buffer = await file.arrayBuffer();
const bytes = new Uint8Array(buffer);
const value = (bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3];
Writing files:
const buffer = new ArrayBuffer(size);
const view = new DataView(buffer);
view.setUint32(0, value, true); // little-endian
Image processing:
const imageData = ctx.getImageData(0, 0, w, h);
const pixels = imageData.data;
for (let i = 0; i < pixels.length; i += 4) {
pixels[i] = transform(pixels[i]); // R, G, B
}
ctx.putImageData(imageData, 0, 0);
What's Next?
Project ideas to try:
- JPEG Info Reader - Similar to PNG reader
- Multi-tone Audio - Mix multiple frequencies
- More Image Filters - Sepia, blur, brightness
- BMP File Creator - Simpler format than PNG
- Text to Audio - Convert text to Morse code beeps
Final Thoughts
You've completed the Typed Arrays series! You now understand:
- How Typed Arrays provide typed access to binary data
- When to use each of the 9 types
- How to build real applications that parse and create binary files
These skills apply to countless real-world scenarios: file converters, image editors, audio tools, network protocols, game save files, and more.
The key is recognizing that all binary data—whether it's a PNG image, WAV audio, or network packet—is just bytes. With Typed Arrays, you can read, write, and manipulate those bytes effectively.
Keep building! Take these patterns and apply them to formats that interest you. Binary data is no longer mysterious—you have the tools to understand and work with it.