Audio signal analyzer and steganography tool using Discrete Fourier Transform - EPITECH G-CNA-400 Project
- Overview
- Compilation
- Usage a) Analyze mode b) Cypher mode c) Decypher mode
- WAV file format
- Signal processing a) Discrete Fourier Transform b) Cooley-Tukey FFT
- Steganography a) Character set b) Encoding algorithm
- Project architecture a) File tree b) Class diagram
Stone Analysis reads an audio signal from a .wav file and can:
- Analyze the signal — decompose it via FFT and display the N dominant frequencies.
- Cypher a message — hide a secret text inside an audio file without audible difference.
- Decypher a message — extract and reconstruct the hidden text from a cyphered file.
Audio files must be encoded in 16-bit PCM mono at 48 kHz. No external audio or DSP library is used; the FFT and steganography are fully hand-implemented.
Subject is in G-CNA-400_stone_analysis.pdf file.
# Compile the main project
make
# Clean object files
make clean
# Remove everything (objects + binary)
make fclean
# Recompile from scratch
make re
# Compile and run unit tests (Criterion)
make unit_tests
# Coverage report on unit tests
make coverage
# Memory check (Valgrind)
make memcheck./stone_analysis [--analyze | -a] IN_FILE N
[--cypher | -c] IN_FILE OUT_FILE MESSAGE
[--decypher| -d] IN_FILE| Argument | Description |
|---|---|
IN_FILE |
Input .wav audio file |
OUT_FILE |
Output .wav file (cypher mode only) |
MESSAGE |
Text to hide in the audio file |
N |
Number of top frequencies to display |
Errors are printed to
stderrand the program exits with code 84.
Decomposes the audio signal via FFT and prints the N dominant frequencies in decreasing order of magnitude.
./stone_analysis --analyze input.wav 3Top 3 frequencies:
494.0 Hz
330.0 Hz
415.0 Hz
Hides a secret message inside the audio file. The output file is identical in size to the input and has the exact same WAV header.
./stone_analysis --cypher input.wav output.wav "Miaou"$ du -b input.wav output.wav
368492 input.wav
368492 output.wav- Accepts letters (a–z, A–Z), digits (0–9), and spaces.
- The message is case-insensitive at encoding; it is decoded in uppercase.
Extracts and prints the message hidden in a previously cyphered file.
./stone_analysis --decypher output.wavMIAOU
The program only accepts 16-bit PCM mono WAV at 48 kHz. Files that do not match this spec are rejected with an error.
| Field | Expected value |
|---|---|
chunkId |
RIFF |
format |
WAVE |
audioFormat |
1 (PCM) |
numChannels |
1 (mono) |
sampleRate |
48000 Hz |
bitsPerSample |
16 |
The header is read as a raw 44-byte struct. For cypher mode, the output header is copied byte-for-byte from the input.
The DFT decomposes a discrete time-domain signal
The magnitude at bin
where
A naïve DFT has complexity
The input is zero-padded to the next power of 2, then processed in-place using the butterfly operation:
for each stage (length = 2, 4, 8, … N):
for each block of size `length`:
w = twiddle factor e^(-j·2π/length)
butterfly(a[i+j], a[i+j+length/2], w^j)
This brings analysis of a 184 000-sample file from ~30 s down to < 0.3 s.
For the IDFT, the same algorithm is applied with the sign of the exponent inverted and a final division by
| Characters | Indices |
|---|---|
a–z / A–Z |
0–25 |
0–9 |
26–35 |
(space) |
36 |
Each character is encoded on 6 bits (2^6 = 64 > 37 values).
The algorithm uses spread-spectrum LSB steganography:
- The message length is encoded first as a 16-bit integer.
- Each character is then encoded as a 6-bit index.
- Each bit is stored in the LSB of the audio sample at position:
with
Using a large prime as the stride distributes the bits evenly across the entire file (spread-spectrum technique), making the hidden data statistically indistinguishable from natural signal noise. The modification is a ±1 change on a 16-bit sample, which is imperceptible to the ear (
The output file is always exactly the same size as the input.
.
├── include/
│ ├── Core.hpp # Argument parsing, mode dispatch
│ ├── Error.hpp # Custom exception class
│ ├── SignalProcessor.hpp # FFT / IFFT / frequency analysis
│ ├── Steganographer.hpp # Spread-spectrum LSB encode/decode
│ └── WavHandler.hpp # WAV file I/O and header validation
├── src/
│ ├── main.cpp # Entry point
│ ├── Core.cpp
│ ├── Error.cpp
│ ├── SignalProcessor.cpp
│ ├── Steganographer.cpp
│ └── WavHandler.cpp
├── tests/
│ └── unit_tests/ # Criterion unit tests
├── G-CNA-400_stone_analysis.pdf
└── Makefile
classDiagram
direction TB
class Core {
-_config : Config
+Core(argc, argv)
+run() int
-parseArgs(argc, argv) void
-displayHelp() void
-executeAnalyze() void
-executeCypher() void
-executeDecypher() void
}
class Config {
+mode : Mode
+inFile : string
+outFile : string
+message : string
+nTopFreqs : size_t
}
class Mode {
<<enum>>
HELP
ANALYZE
CYPHER
DECYPHER
}
class Error {
-_message : string
+Error(message)
+what() const char*
}
class WavHandler {
-_header : WavHeader
-_samples : vector~int16_t~
+load(filename) void
+save(filename) void
+getSamples() vector~int16_t~
+setSamples(samples) void
+getHeader() WavHeader
-validateHeader() void
}
class WavHeader {
+chunkId[4] : char
+sampleRate : uint32_t
+bitsPerSample : uint16_t
+numChannels : uint16_t
+...
}
class SignalProcessor {
+computeDFT(samples)$ ComplexVector
+computeIDFT(freqs)$ vector~int16_t~
+getTopFrequencies(freqs, n, sr)$ vector~FrequencyMagnitude~
}
class Steganographer {
+cypher(samples, message)$ void
+decypher(samples)$ string
-charToIndex(c)$ uint8_t
-indexToChar(idx)$ char
}
Core --> Config : owns
Core --> WavHandler : uses
Core --> SignalProcessor : uses
Core --> Steganographer : uses
Core ..> Error : throws
WavHandler ..> Error : throws
WavHandler --> WavHeader : owns
Config --> Mode : uses