diff --git a/.gitignore b/.gitignore index 1fab653..7d08562 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,6 @@ vite.config.ts.timestamp-* */venv/* */__pycache__/ -.vscode/ \ No newline at end of file +.vscode/ + +*/uploads/* \ No newline at end of file diff --git a/README.md b/README.md index 38032c3..5e33186 100644 --- a/README.md +++ b/README.md @@ -1 +1,67 @@ # AudioImage + +AudioImage is a powerful web-based tool for Audio Spectrogram Art and Digital Steganography, powered by a Flask backend and Svelte 5 frontend. It transforms audio into visual art and allows for secure, invisible file embedding. + +## Features + +* **Process Visualizer**: A real-time, interactive visualization page that lets you watch and hear the transformation process. + * **Live Waveform**: See the audio signal rendered as a glowing, real-time oscilloscope. + * **Step-by-Step Reveal**: Watch the spectrogram and steganographic image being constructed layer by layer. +* **Audio Art Generation**: Convert MP3/AAC audio files into high-resolution visual spectrograms using Python's `librosa` and `matplotlib`. + * **GPU Acceleration**: Automatically uses `torchaudio` and CUDA if available for lightning-fast processing. +* **Steganography Hider**: Hide secret audio or image files inside a "host" PNG image effectively using LSB (Least Significant Bit) encoding. +* **Universal Decoder**: Decode any image created with AudioImage to retrieve the original hidden files. +* **Secure & Private**: All processing happens locally on your server; files are cleaned up automatically. + +## Requirements + +* Python 3.12+ +* Node.js & npm (or Bun) +* CUDA-enabled GPU (optional, for faster spectrogram generation) + +## Installation + +1. **Backend Setup**: + ```bash + cd server + python3 -m venv venv + source venv/bin/activate + pip install -r requirements.txt + ``` + + *Optional for GPU support:* + ```bash + pip install torch torchaudio --index-url https://download.pytorch.org/whl/cu118 + ``` + *(Adjust CUDA version as needed)* + +2. **Frontend Setup**: + ```bash + # Root directory + npm install + npm run build + ``` + +## Running the Application + +1. Start the Flask server (which serves the frontend): + ```bash + cd server + source venv/bin/activate + python app.py + ``` +2. Open your browser to `http://127.0.0.1:5000`. + +## Architecture + +* **Frontend**: SvelteKit (SPA mode), Svelte 5 Runes, TailwindCSS. + * Uses **Server-Sent Events (SSE)** for real-time progress streaming. + * **Canvas API** & **Web Audio API** for the visualizer. +* **Backend**: Flask, NumPy, PIL, Librosa, PyTorch (optional). +* **Integration**: Flask serves the static Svelte build for a unified deployment experience. + +## Usage Notes + +* **Supported Audio**: MP3, AAC, WAV, FLAC. +* **Supported Images**: PNG (host/stego). +* **Visualizer**: Visit the `/visualizer` page or click the link in the header to see the magic happen in real-time. diff --git a/bun.lock b/bun.lock index ef3b58f..e5aa209 100644 --- a/bun.lock +++ b/bun.lock @@ -5,6 +5,7 @@ "": { "name": "audioimage", "devDependencies": { + "@iconify/svelte": "^5.2.1", "@sveltejs/adapter-auto": "^7.0.0", "@sveltejs/adapter-static": "^3.0.10", "@sveltejs/kit": "^2.49.1", @@ -76,6 +77,10 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], + "@iconify/svelte": ["@iconify/svelte@5.2.1", "", { "dependencies": { "@iconify/types": "^2.0.0" }, "peerDependencies": { "svelte": ">5.0.0" } }, "sha512-zHmsIPmnIhGd5gc95bNN5FL+GifwMZv7M2rlZEpa7IXYGFJm/XGHdWf6PWQa6OBoC+R69WyiPO9NAj5wjfjbow=="], + + "@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], diff --git a/package.json b/package.json index 64e72a1..4f480e1 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "version": "0.0.1", "type": "module", "scripts": { - "dev": "vite dev", + "dev": "vite dev --host", "build": "vite build", "preview": "vite preview", "prepare": "svelte-kit sync || echo ''", @@ -14,6 +14,7 @@ "lint": "prettier --check ." }, "devDependencies": { + "@iconify/svelte": "^5.2.1", "@sveltejs/adapter-auto": "^7.0.0", "@sveltejs/adapter-static": "^3.0.10", "@sveltejs/kit": "^2.49.1", diff --git a/server/app.py b/server/app.py index 6f36284..c348f30 100644 --- a/server/app.py +++ b/server/app.py @@ -1,11 +1,13 @@ import os import time -from flask import Flask, request, send_file, jsonify +import json +from flask import Flask, request, send_file, jsonify, send_from_directory, Response from flask_cors import CORS from werkzeug.utils import secure_filename from processor import AudioImageProcessor -app = Flask(__name__) +# Serve the build folder from the parent directory +app = Flask(__name__, static_folder='../build', static_url_path='') CORS(app) # Allow Svelte to communicate # Configuration @@ -19,10 +21,55 @@ def save_upload(file_obj): file_obj.save(path) return path +# --- Frontend Routes --- +@app.route('/') +def index(): + return send_from_directory(app.static_folder, 'index.html') + +@app.errorhandler(404) +def not_found(e): + # If the path starts with /api, return actual 404 + if request.path.startswith('/api/'): + return jsonify({"error": "Not found"}), 404 + # Otherwise return index.html for SPA routing + return send_from_directory(app.static_folder, 'index.html') + @app.route('/health', methods=['GET']) def health(): return jsonify({"status": "ok", "max_mb": 40}) +# --- Background Cleanup --- +import threading + +def cleanup_task(): + """Background thread to clean up old files.""" + expiration_seconds = 600 # 10 minutes + while True: + try: + now = time.time() + if os.path.exists(UPLOAD_FOLDER): + for filename in os.listdir(UPLOAD_FOLDER): + filepath = os.path.join(UPLOAD_FOLDER, filename) + if os.path.isfile(filepath): + # check creation time + if now - os.path.getctime(filepath) > expiration_seconds: + try: + os.remove(filepath) + print(f"Cleaned up: {filename}") + except Exception as e: + print(f"Error cleaning {filename}: {e}") + except Exception as e: + print(f"Cleanup Error: {e}") + + time.sleep(60) # Run every minute + +# Start cleanup thread safely +if os.environ.get('WERKZEUG_RUN_MAIN') == 'true' or not os.environ.get('WERKZEUG_RUN_MAIN'): + # Simple check to try and avoid double threads in reloader, though not perfect + t = threading.Thread(target=cleanup_task, daemon=True) + t.start() + + # --- Endpoint 1: Create Art (Optional: Embed Audio in it) --- @app.route('/api/generate-art', methods=['POST']) def generate_art(): @@ -32,35 +79,48 @@ def generate_art(): audio_file = request.files['audio'] should_embed = request.form.get('embed', 'false').lower() == 'true' + audio_path = None + art_path = None + try: # 1. Save Audio audio_path = save_upload(audio_file) # 2. Generate Art - art_path = processor.generate_spectrogram(audio_path) + min_pixels = 0 + if should_embed: + # Calculate required pixels: File Bytes * 8 (bits) / 3 (channels) + # Add 5% buffer for header and safety + file_size = os.path.getsize(audio_path) + min_pixels = int((file_size * 8 / 3) * 1.05) + + art_path = processor.generate_spectrogram(audio_path, min_pixels=min_pixels) # 3. If Embed requested, run Steganography immediately using the art as host final_path = art_path if should_embed: + # art_path becomes the host, audio_path is the data final_path = processor.encode_stego(audio_path, art_path) + # If we created a new stego image, the pure art_path is intermediate (and audio is input) + # We can delete art_path now if it's different (it is) + if art_path != final_path: + try: os.remove(art_path) + except: pass + return send_file(final_path, mimetype='image/png') + except ValueError as e: + return jsonify({"error": str(e)}), 400 except Exception as e: return jsonify({"error": str(e)}), 500 + finally: + # Cleanup Inputs + if audio_path and os.path.exists(audio_path): + try: os.remove(audio_path) + except: pass + -# --- Endpoint 2: Format Shift (Audio -> Static) --- -@app.route('/api/shift', methods=['POST']) -def shift_format(): - if 'file' not in request.files: - return jsonify({"error": "No file provided"}), 400 - - try: - f_path = save_upload(request.files['file']) - img_path = processor.encode_shift(f_path) - return send_file(img_path, mimetype='image/png') - except Exception as e: - return jsonify({"error": str(e)}), 500 # --- Endpoint 3: Steganography (Audio + Custom Host) --- @app.route('/api/hide', methods=['POST']) @@ -68,14 +128,25 @@ def hide_data(): if 'data' not in request.files or 'host' not in request.files: return jsonify({"error": "Requires 'data' and 'host' files"}), 400 + data_path = None + host_path = None try: data_path = save_upload(request.files['data']) host_path = save_upload(request.files['host']) stego_path = processor.encode_stego(data_path, host_path) return send_file(stego_path, mimetype='image/png') + except ValueError as e: + return jsonify({"error": str(e)}), 400 except Exception as e: return jsonify({"error": str(e)}), 500 + finally: + if data_path and os.path.exists(data_path): + try: os.remove(data_path) + except: pass + if host_path and os.path.exists(host_path): + try: os.remove(host_path) + except: pass # --- Endpoint 4: Decode (Universal) --- @app.route('/api/decode', methods=['POST']) @@ -83,6 +154,7 @@ def decode(): if 'image' not in request.files: return jsonify({"error": "No image provided"}), 400 + img_path = None try: img_path = save_upload(request.files['image']) restored_path = processor.decode_image(img_path) @@ -90,8 +162,118 @@ def decode(): # Determine mimetype based on extension for browser friendliness filename = os.path.basename(restored_path) return send_file(restored_path, as_attachment=True, download_name=filename) + except ValueError as e: + return jsonify({"error": str(e)}), 400 except Exception as e: return jsonify({"error": str(e)}), 500 + finally: + if img_path and os.path.exists(img_path): + try: os.remove(img_path) + except: pass + +# --- Endpoint 4: Visualizer SSE Stream --- +@app.route('/api/visualize', methods=['POST']) +def visualize(): + """ + SSE endpoint that streams the spectrogram generation process. + Returns step-by-step updates for visualization. + """ + if 'audio' not in request.files: + return jsonify({"error": "No audio file provided"}), 400 + + audio_file = request.files['audio'] + audio_path = None + + try: + audio_path = save_upload(audio_file) + file_size = os.path.getsize(audio_path) + min_pixels = int((file_size * 8 / 3) * 1.05) + + def generate_steps(): + art_path = None + final_path = None + + try: + import base64 + + # Step 1: Audio loaded + yield f"data: {json.dumps({'step': 1, 'status': 'loading', 'message': 'Loading audio file...', 'progress': 10})}\n\n" + time.sleep(0.8) + + yield f"data: {json.dumps({'step': 1, 'status': 'complete', 'message': f'Audio loaded: {audio_file.filename}', 'progress': 20, 'fileSize': file_size})}\n\n" + time.sleep(0.5) + + # Step 2: Analyzing audio + yield f"data: {json.dumps({'step': 2, 'status': 'loading', 'message': 'Analyzing audio frequencies...', 'progress': 30})}\n\n" + time.sleep(1.0) + + yield f"data: {json.dumps({'step': 2, 'status': 'complete', 'message': 'Frequency analysis complete', 'progress': 40})}\n\n" + time.sleep(0.5) + + # Step 3: Generating spectrogram + yield f"data: {json.dumps({'step': 3, 'status': 'loading', 'message': 'Generating spectrogram image...', 'progress': 50})}\n\n" + + print(f"[VISUALIZE] Starting spectrogram generation for {audio_path}") + art_path = processor.generate_spectrogram(audio_path, min_pixels=min_pixels) + print(f"[VISUALIZE] Spectrogram generated at {art_path}") + + # Read the spectrogram image and encode as base64 + with open(art_path, 'rb') as img_file: + spectrogram_b64 = base64.b64encode(img_file.read()).decode('utf-8') + print(f"[VISUALIZE] Spectrogram base64 length: {len(spectrogram_b64)}") + + yield f"data: {json.dumps({'step': 3, 'status': 'complete', 'message': 'Spectrogram generated!', 'progress': 70, 'spectrogramImage': f'data:image/png;base64,{spectrogram_b64}'})}\n\n" + print("[VISUALIZE] Sent spectrogram image") + time.sleep(2.0) # Pause to let user see the spectrogram + + # Step 4: Embedding audio + yield f"data: {json.dumps({'step': 4, 'status': 'loading', 'message': 'Embedding audio into image (LSB steganography)...', 'progress': 80})}\n\n" + + final_path = processor.encode_stego(audio_path, art_path) + + # Read the final image and encode as base64 + with open(final_path, 'rb') as img_file: + final_b64 = base64.b64encode(img_file.read()).decode('utf-8') + + yield f"data: {json.dumps({'step': 4, 'status': 'complete', 'message': 'Audio embedded successfully!', 'progress': 95, 'finalImage': f'data:image/png;base64,{final_b64}'})}\n\n" + time.sleep(2.0) # Pause to let user see the final image + + # Step 5: Complete - send the result URL + result_id = os.path.basename(final_path) + yield f"data: {json.dumps({'step': 5, 'status': 'complete', 'message': 'Process complete!', 'progress': 100, 'resultId': result_id})}\n\n" + + except Exception as e: + yield f"data: {json.dumps({'step': 0, 'status': 'error', 'message': str(e), 'progress': 0})}\n\n" + finally: + # Clean up intermediate files (but keep final) + if art_path and art_path != final_path and os.path.exists(art_path): + try: os.remove(art_path) + except: pass + if audio_path and os.path.exists(audio_path): + try: os.remove(audio_path) + except: pass + + response = Response(generate_steps(), mimetype='text/event-stream') + response.headers['Cache-Control'] = 'no-cache' + response.headers['X-Accel-Buffering'] = 'no' + response.headers['Connection'] = 'keep-alive' + return response + + except Exception as e: + if audio_path and os.path.exists(audio_path): + try: os.remove(audio_path) + except: pass + return jsonify({"error": str(e)}), 500 + + +@app.route('/api/result/', methods=['GET']) +def get_result(result_id): + """Serve the result image by ID.""" + result_path = os.path.join(app.config['UPLOAD_FOLDER'], result_id) + if os.path.exists(result_path): + return send_file(result_path, mimetype='image/png', as_attachment=False) + return jsonify({"error": "Result not found"}), 404 + if __name__ == '__main__': # Threaded=True is important for processing images without blocking diff --git a/server/processor.py b/server/processor.py index bd6882b..90bab04 100644 --- a/server/processor.py +++ b/server/processor.py @@ -1,4 +1,5 @@ import os +import time import struct import math import numpy as np @@ -36,49 +37,90 @@ class AudioImageProcessor: return struct.pack(HEADER_FMT, signature, file_size, len(ext_bytes)) + ext_bytes # --- Feature 1: Spectrogram Art --- - def generate_spectrogram(self, audio_path): + def generate_spectrogram(self, audio_path, min_pixels=0): """Generates a visual spectrogram from audio.""" + try: + import torch + import torchaudio + has_torch = True + except ImportError: + has_torch = False + + if has_torch and torch.cuda.is_available(): + try: + # GPU Accelerated Path + device = "cuda" + waveform, sr = torchaudio.load(audio_path) + waveform = waveform.to(device) + + # Create transformation + # Mimic librosa defaults roughly: n_fft=2048, hop_length=512 + n_fft = 2048 + win_length = n_fft + hop_length = 512 + n_mels = 128 + + mel_spectrogram = torchaudio.transforms.MelSpectrogram( + sample_rate=sr, + n_fft=n_fft, + win_length=win_length, + hop_length=hop_length, + n_mels=n_mels, + f_max=8000 + ).to(device) + + S = mel_spectrogram(waveform) + S_dB = torchaudio.transforms.AmplitudeToDB()(S) + + # Back to CPU for plotting + S_dB = S_dB.cpu().numpy()[0] # Take first channel + # Librosa display expects numpy + except Exception as e: + # Fallback to CPU/Librosa if any error occurs + print(f"GPU processing failed, falling back to CPU: {e}") + return self._generate_spectrogram_cpu(audio_path, min_pixels) + else: + return self._generate_spectrogram_cpu(audio_path, min_pixels) + + # Plotting (Common) + return self._plot_spectrogram(S_dB, sr, min_pixels) + + def _generate_spectrogram_cpu(self, audio_path, min_pixels=0): y, sr = librosa.load(audio_path) - S = librosa.feature.melspectrogram(y=y, sr=sr, n_mels=256, fmax=8000) + S = librosa.feature.melspectrogram(y=y, sr=sr, n_mels=128, fmax=8000) S_dB = librosa.power_to_db(S, ref=np.max) - - plt.figure(figsize=(12, 6)) - plt.axis('off') - plt.margins(0, 0) - plt.gca().xaxis.set_major_locator(plt.NullLocator()) - plt.gca().yaxis.set_major_locator(plt.NullLocator()) + return self._plot_spectrogram(S_dB, sr, min_pixels) - # 'magma' is a nice default, but you could parameterize this - librosa.display.specshow(S_dB, sr=sr, fmax=8000, cmap='magma') + def _plot_spectrogram(self, S_dB, sr, min_pixels=0): + # Calculate DPI dynamically to ensure we have enough pixels for steganography + dpi = 300 + if min_pixels > 0: + # Figure is 12x6 inches. Area = 72 sq inches. + # Total Pixels = 72 * dpi^2 + required_dpi = math.ceil((min_pixels / 72) ** 0.5) + # Add a small buffer + dpi = max(dpi, int(required_dpi * 1.05)) - output_path = os.path.join(self.upload_folder, f"art_{os.path.basename(audio_path)}.png") - plt.savefig(output_path, bbox_inches='tight', pad_inches=0, dpi=300) + # Use exact dimensions without margins + width_in = 12 + height_in = 6 + fig = plt.figure(figsize=(width_in, height_in)) + + # Add axes covering the entire figure [left, bottom, width, height] + ax = plt.axes([0, 0, 1, 1], frameon=False) + ax.set_axis_off() + + # 'magma' is a nice default + librosa.display.specshow(S_dB, sr=sr, fmax=8000, cmap='magma', ax=ax) + + output_path = os.path.join(self.upload_folder, f"art_{int(time.time())}.png") + + # specific DPI, no bbox_inches='tight' (which shrinks the image) + plt.savefig(output_path, dpi=dpi) plt.close() return output_path - # --- Feature 2: Format Shift (Raw Data to Image) --- - def encode_shift(self, file_path): - file_data = self._get_bytes(file_path) - file_size = len(file_data) - - header = self._create_header(SIG_SHIFT, file_size, file_path) - payload = header + file_data - - # Calculate size - pixels = math.ceil(len(payload) / 3) - side = math.ceil(math.sqrt(pixels)) - padding = (side * side * 3) - len(payload) - - # Pad and Reshape - arr = np.frombuffer(payload, dtype=np.uint8) - if padding > 0: - arr = np.pad(arr, (0, padding), 'constant') - - img = Image.fromarray(arr.reshape((side, side, 3)), 'RGB') - - output_path = os.path.join(self.upload_folder, f"shift_{os.path.basename(file_path)}.png") - img.save(output_path, "PNG") - return output_path + # --- Feature 3: Steganography (Embed in Host) --- def encode_stego(self, data_path, host_path): diff --git a/server/requirements.txt b/server/requirements.txt index e7cc914..b1f439f 100644 --- a/server/requirements.txt +++ b/server/requirements.txt @@ -4,3 +4,5 @@ numpy Pillow librosa matplotlib +torch +torchaudio diff --git a/src/lib/components/DecoderTool.svelte b/src/lib/components/DecoderTool.svelte new file mode 100644 index 0000000..8bf3f8f --- /dev/null +++ b/src/lib/components/DecoderTool.svelte @@ -0,0 +1,155 @@ + + +
+

+ + Universal Decoder +

+

+ Upload ANY image created with this tool to retrieve the original audio file. +

+ +
e.preventDefault()} + ondrop={(e) => handleFileDrop(e, (f) => (decodeImageFile = f))} + onclick={() => !loading && document.getElementById('decodeInput')?.click()} + onkeydown={(e) => handleKeyEnter(e, 'decodeInput')} + > + { + const target = e.target as HTMLInputElement; + if (target && target.files) { + decodeImageFile = target.files[0]; + } + }} + /> + + {#if decodeImageFile} +
{decodeImageFile.name}
+ {:else} +
Drop Stego-Image to Decode
+ {/if} +
+ + +
diff --git a/src/lib/components/EncoderTool.svelte b/src/lib/components/EncoderTool.svelte new file mode 100644 index 0000000..6c3218c --- /dev/null +++ b/src/lib/components/EncoderTool.svelte @@ -0,0 +1,120 @@ + + +
+

Stego Hider

+
+

+ Hide a audio file inside an innocent looking host image. +

+ +
+ + {#each ['Payload File (Secret)', 'Host Image (Cover)'] as label, i} +
+
+ + + {#if i === 1} +
+ {#if stegoDataFile} +
+ Min: {((stegoDataFile.size * 8) / 3 / 1000000).toFixed(2)} MP +
+ {/if} + {#if stegoHostFile && hostImageMP > 0} +
+ Current: {hostImageMP.toFixed(2)} MP +
+ {/if} +
+ {/if} +
+ { + const target = e.target as HTMLInputElement; + if (target && target.files && target.files[0]) { + const file = target.files[0]; + if (i === 0) { + stegoDataFile = file; + } else { + stegoHostFile = file; + // Calculate MP + const img = new Image(); + const url = URL.createObjectURL(file); + img.onload = () => { + hostImageMP = (img.width * img.height) / 1000000; + URL.revokeObjectURL(url); + }; + img.src = url; + } + } + }} + /> +
+ {/each} + + +
+
+
diff --git a/src/lib/components/Footer.svelte b/src/lib/components/Footer.svelte new file mode 100644 index 0000000..cadae7c --- /dev/null +++ b/src/lib/components/Footer.svelte @@ -0,0 +1,9 @@ + + + diff --git a/src/lib/components/Header.svelte b/src/lib/components/Header.svelte new file mode 100644 index 0000000..84fd3b2 --- /dev/null +++ b/src/lib/components/Header.svelte @@ -0,0 +1,48 @@ + + +
+

+ AudioImage +

+

+ {text} +

+ +
diff --git a/src/lib/components/ResultPreview.svelte b/src/lib/components/ResultPreview.svelte new file mode 100644 index 0000000..664fcb6 --- /dev/null +++ b/src/lib/components/ResultPreview.svelte @@ -0,0 +1,207 @@ + + +
+ +
+ {#if artResultUrl} +
+
+ Generated Spectrogram +
+
+

+ + Spectrogram Result +

+ + Download Image + +
+
+ {/if} + + {#if encoderResultUrl} +
+
+ Steganography Result +
+
+

+ + Stego Result +

+ + Download Stego Image + +
+
+ {/if} +
+ + + {#if decodedAudioUrl} +
+

+ + Decoded Audio Player +

+ +
+ +
+
+ {#if decodedImageUrl} + Decoded Cover + {:else} +
+ +
+ {/if} +
+
+ + +
+ + +
Decoded Track
+
Restored from Image
+ +
+ + + +
+ +
+ {formatTime(currentTime)} + {formatTime(duration)} +
+
+
+ + +
+
+
+ {/if} +
diff --git a/src/lib/components/SpectrogramTool.svelte b/src/lib/components/SpectrogramTool.svelte new file mode 100644 index 0000000..d3ec038 --- /dev/null +++ b/src/lib/components/SpectrogramTool.svelte @@ -0,0 +1,154 @@ + + +
+
+ +
+ +

+ + Audio Art +

+ +

+ Convert your favorite songs into visual spectrograms. Optionally hide the song inside the image + itself! +

+ +
+ +
e.preventDefault()} + ondrop={(e) => handleFileDrop(e, (f) => (audioFile = f))} + onclick={() => !loading && document.getElementById('audioInput')?.click()} + onkeydown={(e) => handleKeyEnter(e, 'audioInput')} + > + { + const target = e.target as HTMLInputElement; + if (target && target.files) audioFile = target.files[0]; + }} + /> + {#if audioFile} +
{audioFile.name}
+ {:else} +
Drag audio here or click to browse
+ {/if} +
+ + + + +
+
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index b138cff..1bba95c 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,142 +1,46 @@ @@ -144,316 +48,86 @@ AudioImage | Steganography Suite -
-
-

- AudioImage -

-

- The ultimate suite for Spectrogram Art and Digital Steganography. Visualize sound, hide data, - and decode secrets. -

-
+
+
{#if errorMsg}
- {errorMsg} - +
+

Error

+

{errorMsg}

+
+
{/if} -
+
-
-
- -
- -

- - Audio Art -

- -

- Convert your favorite songs into visual spectrograms. Optionally hide the song inside the - image itself! -

- -
- -
handleFileDrop(e, (f) => (audioFile = f))} - on:click={() => document.getElementById('audioInput').click()} - > - (audioFile = e.target.files[0])} - /> - {#if audioFile} -
{audioFile.name}
- {:else} -
Drag audio here or click to browse
- {/if} -
- - - - -
-
+ -
-
- - -
- - {#if activeTab === 'shift'} -
-

- Turn ANY file (zip, exe, txt) into a static-noise PNG image. -

-
handleFileDrop(e, (f) => (shiftFile = f))} - on:click={() => document.getElementById('shiftInput').click()} - > - (shiftFile = e.target.files[0])} - /> - {#if shiftFile} -
{shiftFile.name}
- {:else} -
Drag any file here
- / - {/if} -
- -
- {:else} -
-

- Hide a secret file inside an innocent looking host image. -

- -
- - {#each ['Payload File (Secret)', 'Host Image (Cover)'] as label, i} -
- - - i === 0 - ? (stegoDataFile = e.target.files[0]) - : (stegoHostFile = e.target.files[0])} - /> -
- {/each} - - -
-
- {/if} -
+
- -
-

- - Universal Decoder -

-

- Upload ANY image created with this tool to retrieve the original hidden files. -

- -
handleFileDrop(e, (f) => (decodeImageFile = f))} - on:click={() => document.getElementById('decodeInput').click()} - > - (decodeImageFile = e.target.files[0])} - /> - - {#if decodeImageFile} -
{decodeImageFile.name}
- {:else} -
Drop Stego-Image to Decode
- {/if} -
- - -
+ + - {#if artResultUrl || encoderResultUrl} -
-

Result Preview

- - {#if artResultUrl} -
-

- Audio Spectrogram -

- Spectrogram - Download Art -
- {/if} - - {#if encoderResultUrl} -
-

- Encoded Image -

- Encoded Result - Download Encoded Image -
- {/if} -
- {/if} - - -
-

Did you know?

-

- The "Format Shift" tool visualizes raw data bytes as colored pixels. A dense file like a - ZIP will look like TV static, while text files might show repeating patterns. -

-
+
-
- +
+
diff --git a/src/routes/how/+page.svelte b/src/routes/how/+page.svelte new file mode 100644 index 0000000..74bf215 --- /dev/null +++ b/src/routes/how/+page.svelte @@ -0,0 +1,329 @@ + + + + How It Works | AudioImage + + + +
+ +
+ + + Back to App + +

+ How It Works +

+

+ A deep dive into the Mathematics and Computer Science concepts driving AudioImage. +

+
+ + +
+ +
+

+ + 1. The Anatomy of Digital Images +

+

+ Linear Algebra & Data Structures +

+ +

+ To a Computer Scientist, an image is not a "picture"; it is a Tensor (a multi-dimensional array). +

+ +

The Data Structure

+

+ A standard color image is a 3-Dimensional Array with the + shape H × W × C: +

+
    +
  • H (Height): The number of rows.
  • +
  • W (Width): The number of columns.
  • +
  • + C (Channels): The depth of the color information (usually + 3 for Red, Green, Blue). +
  • +
+

+ Mathematically, a single pixel at coordinates (i, j) is a vector: +

+
+ P(i,j) = [R, G, B] +
+

+ Where R, G, B are integers typically ranging from 0 + to 255 (8-bit unsigned integers, or + uint8). +

+ +

The Storage Limit

+

Since every channel is 1 byte (8 bits), a single pixel consumes:

+
+ 3 channels × 1 byte = 3 bytes/pixel +
+

+ This is why the capacity calculation is Total_Bytes / 3. We are mapping one byte of your file to one channel of a pixel. +

+
+ + +
+

+ + 2. The Math of LSB Steganography +

+

Bitwise Logic

+ +

+ Steganography relies on the concept of Bit Significance. + In binary, the number 200 is written as + 11001000: +

+
    +
  • + MSB (Most Significant Bit): The leftmost + 1. It represents 2⁷ (128). If you + flip this, the value becomes 72, which is a massive color shift. +
  • +
  • + LSB (Least Significant Bit): The rightmost + 0. It represents 2⁰ (1). If you flip + this, the value becomes 201. +
  • +
+ +
+

The Visual Tolerance Threshold

+

+ The Human Visual System (HVS) cannot distinguish color differences smaller than roughly ±2 + values on the 0–255 scale. Therefore, modifying the LSB (±1) is mathematically invisible + to us. +

+
+ +

The Algorithm: Bitwise Masking

+

+ The code uses Boolean Algebra to inject the data. Let + H + be the Host Byte and S be the Secret Bit. +

+ +

Step 1: Clearing the LSB

+
+ H' = H & 0xFE (binary: 11111110) +
+

+ This forces the last bit to be 0 regardless of what it was before. +

+ +

Step 2: Injection

+
+ H'' = H' | S +
+

+ Since the last bit of H' is guaranteed to be 0, adding S (which is 0 or 1) simply places S + into that slot. +

+ +

The Combined Equation:

+
+ Encoded_Byte = (Host_Byte & 0xFE) | Secret_Bit +
+
+ + +
+

+ + 3. Format Shifting: Dimensionality Transformation +

+

Reshape Operations

+ +

+ When we turn a file directly into an image, we are performing a Reshape Operation. +

+ +

1D to 3D Mapping

+

+ Your file on the hard drive is a 1-Dimensional Stream of bytes: +

+
+ [b₀, b₁, b₂, ..., bₙ₋₁] +
+

+ We need to fold this line into a square block (the image). The math to find the square side + length: +

+
+ S = ⌈√(N / 3)⌉ +
+
    +
  • N is total bytes.
  • +
  • Divisor 3 accounts for the RGB channels.
  • +
  • + ⌈ ⌉ is the Ceiling Function, ensuring we have enough space. +
  • +
+ +

The Padding Problem (Modulo Arithmetic)

+

Since N is rarely perfectly divisible, we have a "Remainder."

+
+ Padding_Needed = (S² × 3) - N +
+

+ We fill with zeros. In Computer Science, this is called Zero Padding. Without it, the matrix transformation library (NumPy) would crash because a matrix must + be a perfect rectangle. +

+
+ + +
+

+ + 4. Audio Signal Processing (Spectrograms) +

+

+ Fourier Transforms & Psychoacoustics +

+ +

+ When we generate the "Art," we use a Short-Time Fourier Transform (STFT). +

+ +

The Fourier Transform

+

+ A raw audio file (PCM) records air pressure over Time. + The Fourier Transform converts Time Domain signal into + Frequency Domain + signal. It asks: "What notes (frequencies) make up this sound right now?" +

+
+ X(f) = Σ x(t) × e^(-i2πft) +
+
    +
  • x(t): The audio signal at time t.
  • +
  • + e^(-i2πft): Euler's formula, representing sine/cosine + waves. +
  • +
  • X(f): The intensity of frequency f.
  • +
+ +

The Mel Scale (Psychoacoustics)

+

+ Standard frequency scales are linear (0Hz, 100Hz, 200Hz...). However, human hearing is Logarithmic. We can easily hear the difference between 100Hz and 200Hz, but 10,000Hz and 10,100Hz + sound identical to us. +

+

+ The Mel Scale stretches the bass frequencies and compresses the treble frequencies to match + how humans perceive sound: +

+
+ Mel(f) = 2595 × log₁₀(1 + f/700) +
+
+ + +
+

+ + 5. System Design: Protocol & Metadata +

+

+ Binary Protocols & Complexity Analysis +

+ +

+ To make the decoder "smart" (auto-detecting), we implemented a Binary Protocol. +

+ +

The Header

+

+ Just like an HTTP request has headers before the body, our image has a header before the + payload: +

+
+ [SIG] [SIZE] [EXT_LEN] [EXT] [DATA...] +
+
    +
  • + Magic Bytes: A "File Signature" that reduces false positives. + The probability of the first 4 bytes randomly matching is 1 in ~4 billion. +
  • +
  • + Unsigned Long Long (8 bytes): Stores the file size. Max + value: ~18 Exabytes. This is future-proofing. +
  • +
+ +

Big O Notation (Complexity)

+
+
+

Time Complexity

+

O(n)

+

+ The script iterates over the file bytes once. If the file size doubles, the processing + time doubles. This is linear and efficient. +

+
+
+

Space Complexity

+

O(n)

+

+ We load the file into RAM (byte array) and then create an image array of roughly the + same size. +

+
+
+ +
+

Optimization Note

+

+ For the 40MB limit, this is fine. If we were processing 10GB files, we would need to + switch to Streaming I/O, processing the file in + "chunks" (e.g., 4KB at a time) rather than loading it all at once to prevent Out Of Memory + (OOM) errors. +

+
+
+
+ + + +
diff --git a/src/routes/layout.css b/src/routes/layout.css index cd47b6c..556ed4c 100644 --- a/src/routes/layout.css +++ b/src/routes/layout.css @@ -8,9 +8,11 @@ --bg-card: #131318; --bg-glass: rgba(19, 19, 24, 0.7); - --primary: #6366f1; /* Indigo */ + --primary: #6366f1; + /* Indigo */ --primary-glow: rgba(99, 102, 241, 0.4); - --accent: #ec4899; /* Pink */ + --accent: #ec4899; + /* Pink */ --accent-glow: rgba(236, 72, 153, 0.4); --text-main: #ffffff; @@ -43,14 +45,12 @@ h4 { margin-top: 0; } -/* Glassmorphism Utilities */ +/* Card Styles (Solid Dark) */ .glass-panel { - background: var(--bg-glass); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); + background: var(--bg-card); border: 1px solid var(--border-subtle); border-radius: 16px; - box-shadow: 0 4px 30px rgba(0, 0, 0, 0.3); + box-shadow: 0 4px 30px rgba(0, 0, 0, 0.4); } .glass-input { @@ -72,7 +72,7 @@ h4 { /* Button Styles */ .btn-primary { - background: linear-gradient(135deg, var(--primary), #4f46e5); + background: var(--primary); color: white; border: none; padding: 0.75rem 1.5rem; @@ -81,7 +81,8 @@ h4 { cursor: pointer; transition: transform 0.1s ease, - box-shadow 0.2s ease; + box-shadow 0.2s ease, + background-color 0.2s ease; text-transform: uppercase; font-size: 0.875rem; letter-spacing: 0.05em; @@ -90,6 +91,7 @@ h4 { .btn-primary:hover { transform: translateY(-1px); box-shadow: 0 4px 20px var(--primary-glow); + background: #5558e8; } .btn-primary:active { @@ -117,7 +119,19 @@ h4 { .container { max-width: 1200px; margin: 0 auto; - padding: 2rem; + padding: 1rem; +} + +@media (min-width: 640px) { + .container { + padding: 1.5rem; + } +} + +@media (min-width: 768px) { + .container { + padding: 2rem; + } } .grid-cols-2 { @@ -133,13 +147,20 @@ h4 { } .section-title { - font-size: 1.5rem; - margin-bottom: 1.5rem; + font-size: 1.25rem; + margin-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem; } +@media (min-width: 640px) { + .section-title { + font-size: 1.5rem; + margin-bottom: 1.5rem; + } +} + .gradient-text { background: linear-gradient(to right, var(--primary), var(--accent)); -webkit-background-clip: text; @@ -162,4 +183,4 @@ h4 { to { transform: rotate(360deg); } -} +} \ No newline at end of file diff --git a/src/routes/visualizer/+page.svelte b/src/routes/visualizer/+page.svelte new file mode 100644 index 0000000..b66a66e --- /dev/null +++ b/src/routes/visualizer/+page.svelte @@ -0,0 +1,576 @@ + + + + Visualizer | AudioImage + + + +
+ +
+ + + Back to App + +

+ Process Visualizer +

+

+ Watch the transformation of audio into a steganographic image. +

+
+ + +
+ + {#if !spectrogramImage && !finalImage && !isProcessing} +
+

+ + Select Audio File +

+ +
+ +
+ + +
+ {/if} + + + {#if isProcessing} +
+ + {#if audioUrl} +
+ + + + +
+
+ + +
+ Playing Audio... +
+
+ {/if} + +
+
+ + {#if currentStep === 1}Loading audio... + {:else if currentStep === 2}Analyzing frequencies... + {:else if currentStep === 3}Generating spectrogram... + {:else if currentStep === 4}Embedding audio... + {:else if currentStep === 5}Complete! + {:else}Processing...{/if} + + {progress}% +
+
+
+
+
+
+ {/if} + + + {#if spectrogramImage || finalImage || isProcessing} +
+
+

+ + Image Preview +

+ {#if !isProcessing && (spectrogramImage || finalImage)} + + {/if} +
+ +
+ +
+
+ Spectrogram +
+
+ {#if spectrogramImage} +
+ Generated Spectrogram +
+ {:else if isProcessing && currentStep >= 3} +
+
+
+

Generating...

+
+
+ {:else} +
+ +
+ {/if} +
+

+ {spectrogramImage ? 'Raw spectrogram' : 'Waiting...'} +

+
+ + +
+
+ With Hidden Audio +
+
+ {#if finalImage} +
+ Spectrogram with embedded audio +
+ {:else if isProcessing && currentStep >= 4} +
+
+
+

Embedding...

+
+
+ {:else} +
+ +
+ {/if} +
+

+ {finalImage ? '✓ Audio hidden inside!' : 'Waiting...'} +

+
+
+ + + {#if resultId && !isProcessing} + + {/if} +
+ {/if} + + + {#if error} +
+
+ + Error +
+

{error}

+ +
+ {/if} +
+ + + +
+ + diff --git a/tsconfig.json b/tsconfig.json index 2c2ed3c..feea18b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,9 +12,4 @@ "strict": true, "moduleResolution": "bundler" } - // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias - // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files - // - // To make changes to top-level options such as include and exclude, we recommend extending - // the generated config; see https://svelte.dev/docs/kit/configuration#typescript }