Repository: scipy/scipy

Issue:
reading .wav fails when the file is too big on python 3.6.0
Running this on python 3.6.0:

```python
from scipy.io import wavfile
sampleRate, audioData = wavfile.read("output.wav")
```

gives this error:

```
Traceback (most recent call last):
  File "/Users/wyattblue/test.py", line 34
    sampleRate, audioData = wavfile.read("output.wav")
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/scipy/io/wavfile.py", line 592, in read
    is_big_endian, mmap)
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/scipy/io/wavfile.py", line 408, in _read_data_chunk
    data = numpy.frombuffer(fid.read(size), dtype=dtype)
OSError: [Errno 22] Invalid argument
```

output.wav is a 4 hour wavfile. This error doesn't occur when the file is only about 40 minutes long.
This error did not happen when I ran the same code with the same file but on python 3.8.3. I didn't test any other python version.

With some experimentation, I found out that `fid.read(size)` is causing the error but I don't know why.


To get the same file that I have, get ffmpeg and youtube-dl and run this code:

```python
import subprocess

cmd = ["youtube-dl", "-f", "bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4",
       "https://www.youtube.com/watch?v=od-6uhIMRDQ", "--output", "video.mp4"]
subprocess.call(cmd)

cmd = ['ffmpeg', '-i', myInput, '-ab', '160k', '-ac', '2', '-ar', 
       '48000', '-vn', f'output.wav', '-nostats', '-loglevel', '0']
subprocess.call(cmd)
```

#### Notes
Both python versions are 64-bit
I am using the latest scipy version.



Discussion:

Resolving patch:
diff --git a/scipy/io/wavfile.py b/scipy/io/wavfile.py
--- a/scipy/io/wavfile.py
+++ b/scipy/io/wavfile.py
@@ -8,6 +8,7 @@
 `write`: Write a NumPy array as a WAV file.
 
 """
+import io
 import sys
 import numpy
 import struct
@@ -419,6 +420,7 @@ def _read_data_chunk(fid, format_tag, channels, bit_depth, is_big_endian,
 
     # Number of bytes per sample (sample container size)
     bytes_per_sample = block_align // channels
+    n_samples = size // bytes_per_sample
     if bit_depth == 8:
         dtype = 'u1'
     else:
@@ -427,12 +429,16 @@ def _read_data_chunk(fid, format_tag, channels, bit_depth, is_big_endian,
         else:
             dtype = f'{fmt}f{bytes_per_sample}'
 
+    start = fid.tell()
     if not mmap:
-        data = numpy.frombuffer(fid.read(size), dtype=dtype)
+        try:
+            data = numpy.fromfile(fid, dtype=dtype, count=n_samples)
+        except io.UnsupportedOperation:  # not a C-like file
+            fid.seek(start, 0)  # just in case it seeked, though it shouldn't
+            data = numpy.frombuffer(fid.read(size), dtype=dtype)
     else:
-        start = fid.tell()
         data = numpy.memmap(fid, dtype=dtype, mode='c', offset=start,
-                            shape=(size//bytes_per_sample,))
+                            shape=(n_samples,))
         fid.seek(start + size)
 
     _handle_pad_byte(fid, size)
@@ -491,7 +497,7 @@ def _handle_pad_byte(fid, size):
 
 def read(filename, mmap=False):
     """
-    Open a WAV file
+    Open a WAV file.
 
     Return the sample rate (in samples/sec) and data from a WAV file.
 
@@ -512,7 +518,9 @@ def read(filename, mmap=False):
     data : numpy array
         Data read from wav file. Data-type is determined from the file;
         see Notes.  Data is 1-D for 1-channel WAV, or 2-D of shape
-        (Nsamples, Nchannels) otherwise.
+        (Nsamples, Nchannels) otherwise. If a file-like input without a
+        C-like file descriptor (e.g., :class:`python:io.BytesIO`) is
+        passed, this will not be writeable.
 
     Notes
     -----


Test patch:
diff --git a/scipy/io/tests/test_wavfile.py b/scipy/io/tests/test_wavfile.py
--- a/scipy/io/tests/test_wavfile.py
+++ b/scipy/io/tests/test_wavfile.py
@@ -1,11 +1,11 @@
 import os
 import sys
-import tempfile
 from io import BytesIO
 
 import numpy as np
 from numpy.testing import (assert_equal, assert_, assert_array_equal,
                            suppress_warnings)
+import pytest
 from pytest import raises, warns
 
 from scipy.io import wavfile
@@ -120,11 +120,10 @@ def test_read_early_eof_with_data():
         with open(datafile(filename), 'rb') as fp:
             with warns(wavfile.WavFileWarning, match='Reached EOF'):
                 rate, data = wavfile.read(fp, mmap=mmap)
-
-                assert_(data.size > 0)
-                assert_equal(rate, 44100)
-
-        del data
+                assert data.size > 0
+                assert rate == 44100
+                # also test writing (gh-12176)
+                data[0] = 0
 
 
 def test_read_early_eof():
@@ -145,38 +144,37 @@ def test_read_incomplete_chunk():
                 wavfile.read(fp, mmap=mmap)
 
 
-def _check_roundtrip(realfile, rate, dtype, channels):
+def _check_roundtrip(realfile, rate, dtype, channels, tmpdir):
     if realfile:
-        fd, tmpfile = tempfile.mkstemp(suffix='.wav')
-        os.close(fd)
+        tmpfile = str(tmpdir.join('temp.wav'))
     else:
         tmpfile = BytesIO()
-    try:
-        data = np.random.rand(100, channels)
-        if channels == 1:
-            data = data[:, 0]
-        if dtype.kind == 'f':
-            # The range of the float type should be in [-1, 1]
-            data = data.astype(dtype)
-        else:
-            data = (data*128).astype(dtype)
-
-        wavfile.write(tmpfile, rate, data)
+    data = np.random.rand(100, channels)
+    if channels == 1:
+        data = data[:, 0]
+    if dtype.kind == 'f':
+        # The range of the float type should be in [-1, 1]
+        data = data.astype(dtype)
+    else:
+        data = (data*128).astype(dtype)
 
-        for mmap in [False, True]:
-            rate2, data2 = wavfile.read(tmpfile, mmap=mmap)
+    wavfile.write(tmpfile, rate, data)
 
-            assert_equal(rate, rate2)
-            assert_(data2.dtype.byteorder in ('<', '=', '|'), msg=data2.dtype)
-            assert_array_equal(data, data2)
+    for mmap in [False, True]:
+        rate2, data2 = wavfile.read(tmpfile, mmap=mmap)
 
-            del data2
-    finally:
+        assert_equal(rate, rate2)
+        assert_(data2.dtype.byteorder in ('<', '=', '|'), msg=data2.dtype)
+        assert_array_equal(data, data2)
+        # also test writing (gh-12176)
         if realfile:
-            os.unlink(tmpfile)
+            data2[0] = 0
+        else:
+            with pytest.raises(ValueError, match='read-only'):
+                data2[0] = 0
 
 
-def test_write_roundtrip():
+def test_write_roundtrip(tmpdir):
     for realfile in (False, True):
         for dtypechar in ('i', 'u', 'f', 'g', 'q'):
             for size in (1, 2, 4, 8):
@@ -203,4 +201,5 @@ def test_write_roundtrip():
                         for channels in (1, 2, 5):
                             dt = np.dtype('%s%s%s' % (endianness, dtypechar,
                                                       size))
-                            _check_roundtrip(realfile, rate, dt, channels)
+                            _check_roundtrip(realfile, rate, dt, channels,
+                                             tmpdir)