WAV processing depends on more than the sample values: the sample rate, channel layout, and numeric representation must survive the trip through Python. scipy.io.wavfile exposes those details as ordinary NumPy data and writes the processed array back as an uncompressed WAV file.
The wavfile.read() function returns a sample-rate integer and an array whose shape distinguishes mono from multichannel audio. wavfile.write() uses that array's dtype to choose the output sample format, so an int16 array produces 16-bit PCM while a float32 array produces 32-bit floating-point WAV data.
A generated stereo tone makes the round trip reproducible without an external recording. The final reload checks the written file rather than trusting the in-memory array, and the comparison can fail if the rate or sample values change during export.
Related: Compute an FFT with SciPy
Related: Create a spectrogram with SciPy
Related: Resample a signal with SciPy
import numpy as np from scipy.io import wavfile sample_rate = 8000 duration = 0.02 time = np.arange(int(sample_rate * duration)) / sample_rate channels = np.column_stack( ( 0.4 * np.sin(2 * np.pi * 440 * time), 0.4 * np.sin(2 * np.pi * 660 * time), ) ) source_pcm = np.round(channels * np.iinfo(np.int16).max).astype(np.int16)
The two columns represent the left and right channels. Converting the values to int16 establishes a 16-bit PCM source for the file round trip.
wavfile.write("source.wav", sample_rate, source_pcm) rate, samples = wavfile.read("source.wav") print(f"read: rate={rate}, dtype={samples.dtype}, shape={samples.shape}")
A mono file loads as a one-dimensional array. Stereo and other multichannel files load with shape (samples, channels).
quieter_pcm = (samples.astype(np.int32) // 2).astype(np.int16) wavfile.write("quieter.wav", rate, quieter_pcm)
The wider intermediate dtype prevents 16-bit overflow during arithmetic. Reusing rate preserves playback speed, and converting back to int16 keeps the output as 16-bit PCM.
check_rate, check_samples = wavfile.read("quieter.wav") print( f"written: rate={check_rate}, dtype={check_samples.dtype}, " f"shape={check_samples.shape}" ) print( f"peak amplitude: {np.max(np.abs(samples))} -> " f"{np.max(np.abs(check_samples))}" ) print(f"sample rate preserved: {check_rate == rate}") print(f"written samples match: {np.array_equal(check_samples, quieter_pcm)}")
$ python3 wav_file_read_write.py read: rate=8000, dtype=int16, shape=(160, 2) written: rate=8000, dtype=int16, shape=(160, 2) peak amplitude: 13107 -> 6554 sample rate preserved: True written samples match: True
The second peak is half the source amplitude after integer rounding. Both final boolean checks come from reloading quieter.wav, so either becomes False when the written file does not preserve the expected rate or sample array.