Repository: obspy/obspy

Issue:
Decimation of Some WAV Files Leads to Incorrect Trace Duration
### Avoid duplicates

- [X] I searched existing issues

### Bug Summary

When I read some WAV format files with Obspy and then issue decimate, the end time of the resultant Trace is incorrect. I have only seen this problem with 44100 Hz, 2 channel WAV files. I have seen the issue with audio formats in `pcm_s32le` and `pcm_s16le`.

I have only inspected the end time so I am currently unaware of any other effects that may be the actual cause of the incorrect end time.

### Code to Reproduce

```python
import obspy

# Decimating this WAV leads to problems
# https://www.lynxstudio.com/downloads/e44/sample-wav-file-zip-encoded-44-1khz-pcm-24-stereo/  # noqa
# filename from this Zip is SineWaveMinus16.wav
print('Testing SineWaveMinus16.wav')
st = obspy.read('SineWaveMinus16.wav', format='WAV')
print(st)
print(st.copy().decimate(2))
print(st.copy().decimate(4))
"""ffprobe of `SineWaveMinus16.wav`:
Input #0, wav, from 'SineWaveMinus16.wav':
  Duration: 00:01:00.00, bitrate: 2822 kb/s
  Stream #0:0: Audio: pcm_s32le ([1][0][0][0] / 0x0001), 44100 Hz, 2 channels,
                      s32, 2822 kb/s
"""

# These problems do not occur with all WAV files
# https://www2.cs.uic.edu/~i101/SoundFiles/StarWars60.wav
print('Testing StarWars60.wav')
st = obspy.read('StarWars60.wav', format='WAV')
print(st)
print(st.copy().decimate(2))
print(st.copy().decimate(4))
"""ffprobe of `StarWars60.wav`:
Input #0, wav, from 'StarWars60.wav':
  Duration: 00:01:00.00, bitrate: 352 kb/s
  Stream #0:0: Audio: pcm_s16le ([1][0][0][0] / 0x0001), 22050 Hz, 1 channels,
                      s16, 352 kb/s
"""

# Decimating this WAV leads to problems
# https://mauvecloud.net/sounds/pcm1644s.wav
print('Testing pcm1644s.wav')
st = obspy.read('pcm1644s.wav', format='WAV')
print(st)
print(st.copy().decimate(2))
print(st.copy().decimate(4))
"""ffprobe of `pcm1644s.wav`:
Input #0, wav, from 'pcm1644s.wav':
  Duration: 00:00:06.69, bitrate: 1411 kb/s
  Stream #0:0: Audio: pcm_s16le ([1][0][0][0] / 0x0001), 44100 Hz, 2 channels,
              s16, 1411 kb/s
"""

# Decimating this WAV does NOT lead to problems
# https://mauvecloud.net/sounds/pcm1644m.wav
print('Testing pcm1644m.wav')
st = obspy.read('pcm1644m.wav', format='WAV')
print(st)
print(st.copy().decimate(2))
print(st.copy().decimate(4))
"""ffprobe of `pcm1644m.wav`:
Input #0, wav, from 'pcm1644m.wav':
  Duration: 00:00:06.69, bitrate: 705 kb/s
  Stream #0:0: Audio: pcm_s16le ([1][0][0][0] / 0x0001), 44100 Hz, 1 channels,
               s16, 705 kb/s
"""
```


### Error Traceback

```sh
Testing SineWaveMinus16.wav
1 Trace(s) in Stream:
... | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:59.999977Z | 44100.0 Hz, 2646000 samples
1 Trace(s) in Stream:
... | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:01:59.999955Z | 22050.0 Hz, 2646000 samples
1 Trace(s) in Stream:
... | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:01:59.999909Z | 11025.0 Hz, 1323000 samples
Testing StarWars60.wav
1 Trace(s) in Stream:
... | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:59.999955Z | 22050.0 Hz, 1323000 samples
1 Trace(s) in Stream:
... | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:59.999909Z | 11025.0 Hz, 661500 samples
1 Trace(s) in Stream:
... | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:59.999819Z | 5512.5 Hz, 330750 samples
Testing pcm1644s.wav
1 Trace(s) in Stream:
... | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:06.687256Z | 44100.0 Hz, 294909 samples
1 Trace(s) in Stream:
... | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:13.374512Z | 22050.0 Hz, 294909 samples
1 Trace(s) in Stream:
... | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:13.374512Z | 11025.0 Hz, 147455 samples
```


### ObsPy Version?

1.4.0

### Operating System?

Windows

### Python Version?

3.11.5

### Installation Method?

conda


Discussion:
Interesting. It is actually *not* a problem with the decimation, but with the Trace.stats.npts paramter not synced with the actual data length:
```
In [14]: from obspy import read
In [15]: st = read('https://mauvecloud.net/sounds/pcm1644s.wav')

In [17]: st
Out[17]: 
1 Trace(s) in Stream:
... | 1970-01-01T00:00:00.000000Z - 1970-01-01T00:00:06.687256Z | 44100.0 Hz, 294909 samples

In [18]: len(st[0].data)
Out[18]: 589818

In [19]: st[0].stats.npts
Out[19]: 294909
```

In `Trace.__init__()` the npts value is set with
```
header.setdefault('npts', len(data))
```
This means it can take any value if already present in the provided header dictionary.

Looking into io.wav.core and looking at the output of `fh.getparams()` ouput for the above file
```
_wave_params(nchannels=2, sampwidth=2, framerate=44100, nframes=294909, comptype='NONE', compname='not compressed')
```
I think the problem is that the test file is stereo. Therefore, we have twice as many samples as expected, resulting in the problems you describe.

I think the best fix would be to extract each channel into a separate trace object. Alternatively we could discard one of the channels.

Quick work-around: For the stereo files, after the first decimation with factor 2 you have the correct data from a single channel, I think.

Resolving patch:
diff --git a/obspy/io/wav/core.py b/obspy/io/wav/core.py
--- a/obspy/io/wav/core.py
+++ b/obspy/io/wav/core.py
@@ -88,20 +88,19 @@ def _read_wav(filename, headonly=False, **kwargs):  # @UnusedVariable
     | 7000.0 Hz, 2599 samples
     """
     # read WAV file
-    fh = wave.open(filename, 'rb')
-    try:
+    with wave.open(filename, 'rb') as fh:
         # header information
-        (_nchannel, width, rate, length, _comptype, _compname) = fh.getparams()
+        (nchannel, width, rate, length, _comptype, _compname) = fh.getparams()
         header = {'sampling_rate': rate, 'npts': length}
         if headonly:
-            return Stream([Trace(header=header)])
+            return Stream([Trace(header=header) for _ in range(nchannel)])
         if width not in WIDTH2DTYPE.keys():
             msg = "Unsupported Format Type, word width %dbytes" % width
             raise TypeError(msg)
         data = from_buffer(fh.readframes(length), dtype=WIDTH2DTYPE[width])
-    finally:
-        fh.close()
-    return Stream([Trace(header=header, data=data)])
+    traces = [Trace(header=header, data=data[i::nchannel])
+              for i in range(nchannel)]
+    return Stream(traces)
 
 
 def _write_wav(stream, filename, framerate=7000, rescale=False, width=None,


Test patch: