diff --git a/stegano/console/wav.py b/stegano/console/wav.py
new file mode 100644
index 0000000..40cd29d
--- /dev/null
+++ b/stegano/console/wav.py
@@ -0,0 +1,128 @@
+#!/usr/bin/env python
+# Stegano - Stegano is a pure Python steganography module.
+# Copyright (C) 2010-2025 Cédric Bonhomme - https://www.cedricbonhomme.org
+#
+# For more information : https://github.com/cedricbonhomme/Stegano
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see
+
+__author__ = "Cedric Bonhomme"
+__version__ = "$Revision: 0.7 $"
+__date__ = "$Date: 2016/03/18 $"
+__revision__ = "$Date: 2019/06/04 $"
+__license__ = "GPLv3"
+
+try:
+ from stegano import wav
+except Exception:
+ print("Install stegano: pipx install Stegano")
+
+import argparse
+
+from stegano import tools
+
+def main():
+ parser = argparse.ArgumentParser(prog="stegano-lsb")
+ subparsers = parser.add_subparsers(
+ help="sub-command help", dest="command", required=True
+ )
+
+ # Subparser: Hide
+ parser_hide = subparsers.add_parser("hide", help="hide help")
+ # Original audio
+ parser_hide.add_argument(
+ "-i",
+ "--input",
+ dest="input_audio_file",
+ required=True,
+ help="Input audio file.",
+ )
+ parser_hide.add_argument(
+ "-e",
+ "--encoding",
+ dest="encoding",
+ choices=tools.ENCODINGS.keys(),
+ default="UTF-8",
+ help="Specify the encoding of the message to hide."
+ " UTF-8 (default) or UTF-32LE.",
+ )
+
+ group_secret = parser_hide.add_mutually_exclusive_group(required=True)
+ # Non binary secret message to hide
+ group_secret.add_argument(
+ "-m", dest="secret_message", help="Your secret message to hide (non binary)."
+ )
+ # Binary secret message to hide
+ group_secret.add_argument(
+ "-f", dest="secret_file", help="Your secret to hide (Text or any binary file)."
+ )
+
+ # Audio containing the secret
+ parser_hide.add_argument(
+ "-o",
+ "--output",
+ dest="output_audio_file",
+ required=True,
+ help="Output audio containing the secret.",
+ )
+
+ # Subparser: Reveal
+ parser_reveal = subparsers.add_parser("reveal", help="reveal help")
+ parser_reveal.add_argument(
+ "-i",
+ "--input",
+ dest="input_audio_file",
+ required=True,
+ help="Input audio file.",
+ )
+ parser_reveal.add_argument(
+ "-e",
+ "--encoding",
+ dest="encoding",
+ choices=tools.ENCODINGS.keys(),
+ default="UTF-8",
+ help="Specify the encoding of the message to reveal."
+ " UTF-8 (default) or UTF-32LE.",
+ )
+
+ arguments = parser.parse_args()
+
+ if arguments.command == "hide":
+ if arguments.secret_message is not None:
+ secret = arguments.secret_message
+ elif arguments.secret_file != "":
+ secret = tools.binary2base64(arguments.secret_file)
+
+ wav.hide(
+ input_file=arguments.input_audio_file,
+ message=secret,
+ encoding=arguments.encoding,
+ output_file=arguments.output_audio_file
+ )
+
+ elif arguments.command == "reveal":
+ try:
+ secret = wav.reveal(
+ encoded_wav=arguments.input_audio_file,
+ encoding=arguments.encoding
+ )
+ except IndexError:
+ print("Impossible to detect message.")
+ exit(0)
+ if arguments.secret_binary is not None:
+ data = tools.base642binary(secret)
+ with open(arguments.secret_binary, "wb") as f:
+ f.write(data)
+ else:
+ print(secret)
diff --git a/stegano/exifHeader/exifHeader.py b/stegano/exifHeader/exifHeader.py
index 33fec99..1fefa93 100644
--- a/stegano/exifHeader/exifHeader.py
+++ b/stegano/exifHeader/exifHeader.py
@@ -40,7 +40,7 @@ def hide(
from zlib import compress
if secret_file is not None:
- with open(secret_file) as f:
+ with open(secret_file, "rb") as f:
secret_message = f.read()
try:
diff --git a/stegano/wav/__init__.py b/stegano/wav/__init__.py
new file mode 100644
index 0000000..e2529fe
--- /dev/null
+++ b/stegano/wav/__init__.py
@@ -0,0 +1,5 @@
+#!/usr/bin/env python
+
+from .wav import hide, reveal
+
+__all__ = ["hide", "reveal"]
diff --git a/stegano/wav/wav.py b/stegano/wav/wav.py
new file mode 100644
index 0000000..0593257
--- /dev/null
+++ b/stegano/wav/wav.py
@@ -0,0 +1,105 @@
+#!/usr/bin/env python
+# Stegano - Stéganô is a basic Python Steganography module.
+# Copyright (C) 2010-2024 Cédric Bonhomme - https://www.cedricbonhomme.org
+#
+# For more information : https://github.com/cedricbonhomme/Stegano
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see
+
+__author__ = "Cedric Bonhomme"
+__version__ = "$Revision: 0.2 $"
+__date__ = "$Date: 2010/10/01 $"
+__revision__ = "$Date: 2017/02/06 $"
+__license__ = "GPLv3"
+
+import wave
+from typing import IO, Union
+from stegano import tools
+
+def hide(
+ input_file: Union[str, IO[bytes]],
+ message: str,
+ output_file: Union[str, IO[bytes]],
+ encoding: str = "UTF-8"
+):
+ """
+ Hide a message (string) in a .wav audio file.
+
+ Use the lsb of each PCM encoded sample to hide the message string characters as ASCII values.
+ The first eight bits are used for message_length of the string.
+ """
+ message_length = len(message)
+ assert message_length != 0, "message message_length is zero"
+ assert message_length < 255, "message is too long"
+
+ output = wave.open(output_file, "wb")
+ with wave.open(input_file, "rb") as input:
+ # get .wav params
+ nchannels, sampwidth, framerate, nframes, comptype, _ = input.getparams()
+ assert comptype == "NONE", "only uncompressed files are supported"
+
+ nsamples = nframes * nchannels
+
+ message_bits = f"{message_length:08b}" + "".join(tools.a2bits_list(message, encoding))
+ assert len(message_bits) <= nsamples, "message is too long"
+
+ # copy over .wav params to output
+ output.setnchannels(nchannels)
+ output.setsampwidth(sampwidth)
+ output.setframerate(framerate)
+
+ # encode message in frames
+ frames = bytearray(input.readframes(nsamples))
+ for i in range(nsamples):
+ if i < len(message_bits):
+ if message_bits[i] == "0":
+ frames[i] = frames[i] & ~1
+ else:
+ frames[i] = frames[i] | 1
+
+ # write out
+ output.writeframes(frames)
+
+
+def reveal(input_file: Union[str, IO[bytes]], encoding: str = "UTF-8"):
+ """
+ Find a message in an image.
+
+ Check the lsb of each PCM encoded sample for hidden message characters (ASCII values).
+ The first eight bits are used for message_length of the string.
+ """
+ message = ""
+ encoding_len = tools.ENCODINGS[encoding]
+ with wave.open(input_file, "rb") as input:
+ nchannels, _, _, nframes, comptype, _ = input.getparams()
+ assert comptype == "NONE", "only uncompressed files are supported"
+
+ nsamples = nframes * nchannels
+ frames = bytearray(input.readframes(nsamples))
+
+ # Read first 8 bits for message length
+ length_bits = ""
+ for i in range(8):
+ length_bits += str(frames[i] & 1)
+ message_length = int(length_bits, 2)
+
+ # Read message bits
+ message_bits = ""
+ for i in range(8, 8 + message_length * encoding_len):
+ message_bits += str(frames[i] & 1)
+
+ # Convert bits to string
+ chars = [chr(int(message_bits[i:i+encoding_len], 2)) for i in range(0, len(message_bits), encoding_len)]
+ message = "".join(chars)
+ return message
diff --git a/tests/sample-files/free-software-song.wav b/tests/sample-files/free-software-song.wav
new file mode 100644
index 0000000..405684a
Binary files /dev/null and b/tests/sample-files/free-software-song.wav differ
diff --git a/tests/test_wav.py b/tests/test_wav.py
new file mode 100644
index 0000000..86276d0
--- /dev/null
+++ b/tests/test_wav.py
@@ -0,0 +1,62 @@
+#!/usr/bin/env python
+# Stegano - Stegano is a pure Python steganography module.
+# Copyright (C) 2010-2025 Cédric Bonhomme - https://www.cedricbonhomme.org
+#
+# For more information : https://github.com/cedricbonhomme/Stegano
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see
+
+__author__ = "Cedric Bonhomme"
+__version__ = "$Revision: 0.1 $"
+__date__ = "$Date: 2016/05/19 $"
+__license__ = "GPLv3"
+
+import os
+import unittest
+
+from stegano import wav
+
+
+class TestWav(unittest.TestCase):
+ def test_hide_empty_message(self):
+ """
+ Test hiding the empty string.
+ """
+ with self.assertRaises(AssertionError):
+ wav.hide("./tests/sample-files/free-software-song.wav", "", "./audio.wav")
+
+ def test_hide_and_reveal(self):
+ messages_to_hide = ["a", "foo", "Hello World!", ":Python:"]
+
+ for message in messages_to_hide:
+ wav.hide("./tests/sample-files/free-software-song.wav", message, "./audio.wav")
+ clear_message = wav.reveal("./audio.wav")
+
+ self.assertEqual(message, clear_message)
+
+ def test_with_too_long_message(self):
+ with open("./tests/sample-files/lorem_ipsum.txt") as f:
+ message = f.read()
+ with self.assertRaises(AssertionError):
+ wav.hide("./tests/sample-files/free-software-song.wav", message, "./audio.wav")
+
+ def tearDown(self):
+ try:
+ os.unlink("./audio.wav")
+ except Exception:
+ pass
+
+
+if __name__ == "__main__":
+ unittest.main()