44"""
55
66import wave
7+ # pylint: disable=import-error # numpy may not be available in all environments
78import numpy as np
89from .utils import (
910 validate_file_exists , validate_output_path ,
10- string_to_binary , binary_to_string ,
11- add_delimiter , find_delimiter ,
12- is_valid_audio_format
11+ string_to_binary , add_delimiter ,
12+ is_valid_audio_format , prepare_message_from_file , decode_binary_message
1313)
1414
1515
16+ def _load_wav_data (input_path ):
17+ """Load WAV file data and return audio parameters and data."""
18+ with wave .open (input_path , 'rb' ) as wav_in :
19+ # Get audio parameters
20+ params = {
21+ 'frames' : wav_in .getnframes (),
22+ 'sample_width' : wav_in .getsampwidth (),
23+ 'framerate' : wav_in .getframerate (),
24+ 'channels' : wav_in .getnchannels ()
25+ }
26+
27+ print (f"Audio info: { params ['frames' ]} frames, { params ['sample_width' ]} bytes/sample, "
28+ f"{ params ['channels' ]} channels, { params ['framerate' ]} Hz" )
29+
30+ # Read all audio data
31+ audio_data = wav_in .readframes (params ['frames' ])
32+
33+ return params , audio_data
34+
35+
36+ def _convert_audio_to_array (audio_data , sample_width ):
37+ """Convert audio data to numpy array based on sample width."""
38+ if sample_width == 1 :
39+ return np .frombuffer (audio_data , dtype = np .uint8 )
40+ if sample_width == 2 :
41+ return np .frombuffer (audio_data , dtype = np .int16 )
42+ if sample_width == 4 :
43+ return np .frombuffer (audio_data , dtype = np .int32 )
44+
45+ raise ValueError (f"Unsupported sample width: { sample_width } bytes" )
46+
47+
1648def encode_audio (input_path , output_path , message , file_path = None ):
1749 """
1850 Encode a message or file content into a WAV audio file using LSB steganography
@@ -39,69 +71,39 @@ def encode_audio(input_path, output_path, message, file_path=None):
3971 if not is_valid_audio_format (input_path ):
4072 raise ValueError ("Input must be a WAV audio file" )
4173
42- # Read message from file if file_path provided
43- if file_path :
44- validate_file_exists (file_path )
45- with open (file_path , 'r' , encoding = 'utf-8' ) as f :
46- message = f .read ()
74+ # Prepare message
75+ message = prepare_message_from_file (message , file_path )
4776
48- if not message :
49- raise ValueError ( "Message cannot be empty" )
77+ # Load WAV data
78+ params , audio_data = _load_wav_data ( input_path )
5079
51- # Open WAV file and read audio data
52- with wave .open (input_path , 'rb' ) as wav_in :
53- # Get audio parameters
54- frames = wav_in .getnframes ()
55- sample_width = wav_in .getsampwidth ()
56- framerate = wav_in .getframerate ()
57- channels = wav_in .getnchannels ()
58-
59- print (f"Audio info: { frames } frames, { sample_width } bytes/sample, "
60- f"{ channels } channels, { framerate } Hz" )
61-
62- # Read all audio data
63- audio_data = wav_in .readframes (frames )
80+ # Convert to numpy array
81+ audio_array = _convert_audio_to_array (audio_data , params ['sample_width' ])
6482
65- # Convert to numpy array based on sample width
66- if sample_width == 1 :
67- # 8-bit samples (unsigned)
68- audio_array = np .frombuffer (audio_data , dtype = np .uint8 )
69- elif sample_width == 2 :
70- # 16-bit samples (signed)
71- audio_array = np .frombuffer (audio_data , dtype = np .int16 )
72- elif sample_width == 4 :
73- # 32-bit samples (signed)
74- audio_array = np .frombuffer (audio_data , dtype = np .int32 )
75- else :
76- raise ValueError (f"Unsupported sample width: { sample_width } bytes" )
77-
78- # Check capacity (1 bit per sample)
79- max_chars = (len (audio_array ) // 8 ) - 2 # Reserve space for delimiter
83+ # Check capacity and encode
84+ max_chars = (len (audio_array ) // 8 ) - 2
8085 if len (message ) > max_chars :
8186 raise ValueError (f"Message too long. Maximum capacity: { max_chars } characters, "
8287 f"got: { len (message )} " )
8388
84- # Convert message to binary with delimiter
89+ # Convert message to binary and encode
8590 binary_message = add_delimiter (string_to_binary (message ))
86-
87- # Create a copy of audio data for modification
8891 modified_audio = audio_array .copy ()
8992
9093 # Encode message into LSBs
9194 for i , bit in enumerate (binary_message ):
9295 if i < len (modified_audio ):
93- # Clear LSB and set new bit
94- if sample_width == 1 : # Unsigned 8-bit
96+ if params ['sample_width' ] == 1 :
9597 modified_audio [i ] = (modified_audio [i ] & 0xFE ) | int (bit )
96- else : # Signed 16-bit or 32-bit
97- # For signed integers, we need to be careful with the LSB
98+ else :
9899 modified_audio [i ] = (modified_audio [i ] & ~ 1 ) | int (bit )
99100
100101 # Write encoded audio to output file
101102 with wave .open (output_path , 'wb' ) as wav_out :
102- wav_out .setnchannels (channels )
103- wav_out .setsampwidth (sample_width )
104- wav_out .setframerate (framerate )
103+ # pylint: disable=no-member # wav_out is Wave_write, not Wave_read
104+ wav_out .setnchannels (params ['channels' ])
105+ wav_out .setsampwidth (params ['sample_width' ])
106+ wav_out .setframerate (params ['framerate' ])
105107 wav_out .writeframes (modified_audio .tobytes ())
106108
107109 print (f"✓ Message successfully encoded into { output_path } " )
@@ -110,7 +112,7 @@ def encode_audio(input_path, output_path, message, file_path=None):
110112
111113 return True
112114
113- except Exception as e :
115+ except ( OSError , ValueError , AttributeError , TypeError ) as e :
114116 print (f"✗ Encoding failed: { str (e )} " )
115117 return False
116118
@@ -158,24 +160,9 @@ def decode_audio(input_path):
158160 binary_message += str (sample & 1 ) # Get LSB
159161
160162 # Find delimiter and extract message
161- binary_message = find_delimiter (binary_message )
162-
163- if not binary_message :
164- print ("✗ No hidden message found or message corrupted" )
165- return None
166-
167- # Convert binary to text
168- try :
169- decoded_message = binary_to_string (binary_message )
170- print (f"✓ Message successfully decoded from { input_path } " )
171- print (f" Message length: { len (decoded_message )} characters" )
172- return decoded_message
173-
174- except Exception as e :
175- print (f"✗ Failed to convert binary to text: { str (e )} " )
176- return None
163+ return decode_binary_message (binary_message , input_path )
177164
178- except Exception as e :
165+ except ( OSError , ValueError , AttributeError , TypeError ) as e :
179166 print (f"✗ Decoding failed: { str (e )} " )
180167 return None
181168
@@ -202,6 +189,6 @@ def get_audio_capacity(audio_path):
202189 # 1 bit per sample, 8 bits per character, minus space for delimiter
203190 return (frames // 8 ) - 2
204191
205- except Exception as e :
192+ except ( OSError , AttributeError , ValueError ) as e :
206193 print (f"✗ Failed to calculate capacity: { str (e )} " )
207194 return 0
0 commit comments