Repository: librosa/librosa

Issue:
STFT/ISTFT pre-allocate output buffers
#### Description

STFT allocates an output buffer, but sometimes you might want to have it compute directly into an existing buffer.  For example, in griffin-lim, the method alternates stft/istft for each iterate, which is then discarded.  It would be better if we could give it an `out=` variable, which it would use instead of allocating a new buffer; this way, we could cut down on redundant memory allocations.

If the shape and/or dtype doesn't line up, then it should throw an exception.


Discussion:
This one's actually a bit more subtle than I had initially thought, due to padding/trimming in istft.  I'll punt this one for now.
Subtlety aside, I think this is pretty doable.  It will generally be a pain for a user to know in advance the shape of the output matrix (the aforementioned subtlety), but the main use case for this parameter would be iterative procedures like griffin-lim where the output shape can be inferred from a previous iteration.
@bmcfee Yes. Another use case is when you're processing a database of fixed-duration recordings. This is very frequent in SONYC (10 seconds) and BirdVox (usually 1 second). OpenMIC (10 seconds) also comes to mind.
It would be nice if there was a way to predict the shape of the STFT matrix from the knowledge of:
* physical duration
* sample rate
* hop length
* frame length
* n_fft

so that the same buffer can be re-used (e.g. in a pescador streamer)

Good point. You would also need to know the padding and trimming parameters, but I think a general helper here could be useful.
Started hacking at this over the weekend, a couple of observations:

1. stft is pretty easy
2. istft is complicated because we over-allocate up front and then trim to undo centering.  I think we could streamline this by pre-allocating exactly the amount that we need, and then only filling what there's space for.  To pull this off, I think we'd need to introduce an `offset` parameter to `overlap_add` and `window_sumsquare` so that centering can be countered up front rather than post-hoc.  This would also get us out of having to call `fix_length` at the end of the function (if needed), which invokes either a trim or a copy and is therefore wasteful.
3. Should we support pre-allocation in reassigned spectrogram?  That seems like it could lead to a lot of API bloat.
4. @cwitkowitz and I had an offline conversation about related issues in CQT, ie computing the number of frames up front and potentially preallocating rather than stacking per-octave results.  This is, I think, doable, but much more complicated than in the stft case: the projection onto the cqt basis means that our output storage is A) much smaller than the full spectrum, and B) the spectrum buffer itself wouldn't necessarily get much reuse.  (CQT length calculation is independently useful though, so we should add it anyway.)
Kicking this up to 0.8.1.  It won't break API (only adding new parameters), but it will require some delicate restructuring of internals to do correctly.

Side note: I think this can be combined with an improved solution for #854, where pre-allocation would allow us to stop the inversion loop early (in istft) and eliminate more copies (in icqt).
Note: kicked this up to 0.9.0 because whatever we do will probably interact with #1130 , and need to be redone anyway.
As an aside, numpy's fft interface does not support a preallocated target array, so there will always be some amount of copying involved here.
This might be one of the core reasons why librosa's STFT is lagging behind nnAudio's STFT, even on CPU ?
> This might be one of the core reasons why librosa's STFT is lagging behind nnAudio's STFT, even on CPU ?

Is it?  The benchmarks in their docs seem to me like it's about on par, though no error bars are reported...

https://kinwaicheuk.github.io/nnAudio/intro.html#speed
ah, yes, it's mostly CQT2010 that is behind, not STFT
> ah, yes, it's mostly CQT2010 that is behind, not STFT

Right, but I think most of this could be traced to differences in resampling mode, so huge grains of salt should be applied.
Jotting down some thoughts on this one.

## stft

- If `center=False`, this is pretty easy to handle.  The shape of the stft is easily predicted from the shape of y and the framing parameters. We just need to verify that the provided output array matches, and it'll be good to go.
- If `center=True`, we can probably get a bit of a speed boost by only padding the first and last few frames, rather than the whole signal.  This will require breaking the block loop into three stages to handle whether left or right padding is needed (or neither, for the middle of the signal).  I think this is worth doing independent of preallocation, as it can dramatically reduce the copying induced by padding and thus reduce our memory footprint.  (This applies almost everywhere padding is needed.)

## istft

- This part is a little trickier.  Our ISTFT over-allocates up front, and then trims off the excess as needed.  I think it should be possible to avoid over-allocating as long as we're a little more clever about how the inverse window normalization is handled.  Probably we'll need to change the order of operations between normalizing and trimming; this way we can over-allocate the normalization buffer (because it's easier that way) and then trim it to match whatever we actually need for reconstruction.
Prototype for stft with preallocated output array.  It's a little sloppy, but I'm pretty sure it's correct. Minor speed boost when running on long, multichannel inputs.  The gist is as above: we don't pad the full input signal, but only the head and tail.  We can then use the resulting frame shapes, along with the framed input signal, to determine the shape of the implicitly padded output array.

The frame slicing math is a little delicate, but it basically goes like this:

1. With padding, frames start at positions (in the unpadded, original input) `-n_fft//2, -n_fft//2 + hop_length, -n_fft//2 + k * hop_length, ...`.  Here, `k` denotes the frame index.
2. Frames `k` such that `k * hop_length > n_fft//2` do not depend on left padding.  This defines the start of the "middle" portion of the stft, starting at sample index `start = - n_fft//2 + ceil(n_fft//2/hop_length) * hop_length`.
3. The head of the signal (that will depend on left padding) therefore only needs to see up to the end of the previous frame: `y[... :(ceil(n_fft//2/hop_length) - 1) * hop_length - n_fft//2 + n_fft]`
4. The tail of the signal starts at frame `tail_k = k+1` where `-n_fft//2 + k * hop_length + n_fft < len(y)`; Here `k` is the last frame that fits entirely within the input signal.  This gives us `tail_k = 1 + (len(y) + n_fft//2 - n_fft) // hop_length`.
5. The tail of the signal is right-padded by half a frame, and only needs to see `y[..., tail_k * hop_length - n_fft//2:]`.

Once we have the (padded) head, middle, and (padded) tail, we can frame each part separtely and populate the output array.  All we need to keep track of is a frame offset (=# frames in the head) when mapping the middle's frames to the output frames.

<details>

<summary> STFT with pre-allocated output </summary>

```python
def stft(
    y,
    n_fft=2048,
    hop_length=None,
    win_length=None,
    window="hann",
    center=True,
    dtype=None,
    pad_mode="constant",
    out=None,
):

    # By default, use the entire frame
    if win_length is None:
        win_length = n_fft

    # Set the default hop, if it's not already specified
    if hop_length is None:
        hop_length = int(win_length // 4)

    # Check audio is valid
    util.valid_audio(y, mono=False)

    fft_window = get_window(window, win_length, fftbins=True)

    # Pad the window out to n_fft size
    fft_window = util.pad_center(fft_window, n_fft)

    # Reshape so that the window can be broadcast
    fft_window = util.expand_to(fft_window, ndim=1 + y.ndim, axes=-2)

    # Pad the time series so that frames are centered
    if center:
        if n_fft > y.shape[-1]:
            warnings.warn(
                "n_fft={} is too small for input signal of length={}".format(
                    n_fft, y.shape[-1]
                )
            )

        padding = [(0, 0) for _ in range(y.ndim)]
        padding[-1] = (n_fft // 2, 0)
        #y = np.pad(y, padding, mode=pad_mode)
        
        # How many frames depend on left padding?
        start_k = int(np.ceil(n_fft // 2 / hop_length))
        start = start_k * hop_length - n_fft//2
        
        y_pre = np.pad(y[..., :(start_k - 1) * hop_length - n_fft//2 + n_fft], padding, mode=pad_mode)
        
        # What's the last frame that does not depend on padding?
        padding[-1] = (0, n_fft//2)
        
        tail_k = (len(y) + n_fft//2 - n_fft) // hop_length + 1
        y_post = np.pad(y[..., (tail_k)*hop_length-n_fft//2:], padding, mode=pad_mode)
    else:
        start = 0

    if not center and n_fft > y.shape[-1]:
        raise ParameterError(
            "n_fft={} is too large for input signal of length={}".format(
                n_fft, y.shape[-1]
            )
        )

    fft = get_fftlib()

    if dtype is None:
        dtype = util.dtype_r2c(y.dtype)
    
    # Window the time series.
    y_frames = util.frame(y[..., start:], frame_length=n_fft, hop_length=hop_length)

    # Pre-allocate the STFT matrix
    shape = list(y_frames.shape)
    shape[-2] = 1 + n_fft // 2
    
    if center:
        y_frames_pre = util.frame(y_pre, frame_length=n_fft, hop_length=hop_length)
        y_frames_post = util.frame(y_post, frame_length=n_fft, hop_length=hop_length)
        shape[-1] += y_frames_pre.shape[-1] + y_frames_post.shape[-1]
        
    if out is None:
        stft_matrix = np.zeros(shape, dtype=dtype, order="F")
    elif not np.allclose(out.shape, shape):
        raise ParameterError(f'Shape mismatch for provided output array out.shape={out.shape} != {shape}')
    else:
        stft_matrix = out

    # Fill in the warm-up
    if center:
        off_start = y_frames_pre.shape[-1]
        off_end = y_frames_post.shape[-1]
        
        stft_matrix[..., :off_start] = fft.rfft(fft_window * y_frames_pre, axis=-2)
        stft_matrix[..., -off_end:] = fft.rfft(fft_window * y_frames_post, axis=-2)
    else:
        off_start = 0

    n_columns = util.MAX_MEM_BLOCK // (
        np.prod(y_frames.shape[:-1]) * y_frames.itemsize
    )
    n_columns = max(n_columns, 1)

    for bl_s in range(0, y_frames.shape[-1], n_columns):
        bl_t = min(bl_s + n_columns, y_frames.shape[-1])

        stft_matrix[..., bl_s+off_start:bl_t+off_start] = fft.rfft(
            fft_window * y_frames[..., bl_s:bl_t], axis=-2
        )
    return stft_matrix
```

</details>

I've tested this reasonably thoroughly with the following block to hit short-signal and small-frame edge cases:

```python
for N in [512, 1024, 2048, 10000]:
    y = np.random.randn(N)
    for n_fft in [1023, 1024, 1025]:
        for hop_length in [129, 255, 256, 257, 384, n_fft]:
            D1 = librosa.stft(y, hop_length=hop_length, n_fft=n_fft)
            D2 = stft(y, hop_length=hop_length, n_fft=n_fft)
            assert np.allclose(D1, D2)
```

It also works seamlessly on large, multichannel inputs.  I think this just needs a bit of a cleanup but it's otherwise done.  Next up is ISTFT.
Quick follow-up, benchmarking a 5 minute random noise signal with standard STFT parameters and dtype=float64, we get the following for the current `librosa.stft`:

```python
>>> %timeit librosa.stft(y, center=center, hop_length=hop_length, n_fft=n_fft)
714 ms ± 53.4 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

>>> %memit librosa.stft(y, center=center, hop_length=hop_length, n_fft=n_fft)
peak memory: 980.07 MiB, increment: 199.57 MiB
```
and for the proposed optimization:
```python
>>> %timeit stft(y, center=center, hop_length=hop_length, n_fft=n_fft)
252 ms ± 276 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)

>>> %memit stft(y, center=center, hop_length=hop_length, n_fft=n_fft)
peak memory: 942.08 MiB, increment: 161.55 MiB
```

So a pretty reasonable speedup and memory savings just from avoiding a full copy/pad.

The time savings are less impressive with `dtype=float32`, but the memory savings are still pretty good.
One more follow-up: the savings stack up if you use a pre-allocated output array.

```python
>>> %timeit stft(y, center=center, hop_length=hop_length, n_fft=n_fft, out=out)
172 ms ± 793 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

>>> %memit  stft(y, center=center, hop_length=hop_length, n_fft=n_fft, out=out)
peak memory: 794.28 MiB, increment: 0.00 MiB
```
And here's the istft implementation.

The main changes are as described above:

1. Pre-allocating the output array to the exact length.  Or, verifying that the pre-allocated output array is the correct length.
2. Separating the "head" portion that depends on left-padding from everything else.
3. Over-allocating the ifft_window_sum array and then cutting it down to length before, rather than after normalization.

Unlike the forward STFT, we don't have to worry about tail padding.  However, I did need to tweak the `__overlap_add` helper function to avoid walking off the end of the destination array.  Since that helper is only ever used in istft, I anticipate no other changes.

<details>

<summary> istft with pre-allocated output array </summary>

```python
def istft(
    stft_matrix,
    hop_length=None,
    win_length=None,
    n_fft=None,
    window="hann",
    center=True,
    dtype=None,
    length=None,
    out=None,
):

    if n_fft is None:
        n_fft = 2 * (stft_matrix.shape[-2] - 1)

    # By default, use the entire frame
    if win_length is None:
        win_length = n_fft

    # Set the default hop, if it's not already specified
    if hop_length is None:
        hop_length = int(win_length // 4)

    ifft_window = get_window(window, win_length, fftbins=True)

    # Pad out to match n_fft, and add broadcasting axes
    ifft_window = util.pad_center(ifft_window, n_fft)
    ifft_window = util.expand_to(ifft_window, ndim=stft_matrix.ndim, axes=-2)

    # For efficiency, trim STFT frames according to signal length if available
    if length:
        if center:
            padded_length = length + 2 * (n_fft//2)
        else:
            padded_length = length
        n_frames = min(stft_matrix.shape[-1], int(np.ceil(padded_length / hop_length)))
    else:
        n_frames = stft_matrix.shape[-1]

    if dtype is None:
        dtype = util.dtype_c2r(stft_matrix.dtype)

    shape = list(stft_matrix.shape[:-2])
    expected_signal_len = n_fft + hop_length * (n_frames - 1)
    
    if length:
        expected_signal_len = length
    elif center:
        expected_signal_len -= 2 * (n_fft//2)
        
    shape.append(expected_signal_len)
    
    if out is None:
        y = np.zeros(shape, dtype=dtype)
    elif not np.allclose(out.shape, shape):
        raise ParameterError(f'Shape mismatch for provided output array out.shape={out.shape} != {shape}')
    else:
        y = out
        # Since we'll be doing overlap-add here, this needs to be initialized to zero.
        y.fill(0.)

    fft = get_fftlib()

    if center:        
        # First frame that does not depend on padding
        #  k * hop_length - n_fft//2 >= 0
        # k * hop_length >= n_fft // 2
        # k >= (n_fft//2 / hop_length)
        
        start_frame = int(np.ceil((n_fft//2) / hop_length))
        
        # Do overlap-add on the head block
        ytmp = ifft_window * fft.irfft(stft_matrix[..., :start_frame], n=n_fft, axis=-2)
        shape[-1] = n_fft + hop_length * (start_frame - 1)
        head_buffer = np.zeros(shape, dtype=dtype)
        __overlap_add(head_buffer, ytmp, hop_length)
        
        # Trim off the first n_fft//2 samples from the head and copy into target buffer
        y[..., :shape[-1]-n_fft//2] = head_buffer[..., n_fft//2:]
        
        # This offset compensates for any differences between frame alignment
        # and padding truncation
        offset = start_frame * hop_length - n_fft//2
        
    else:
        start_frame = 0
        offset = 0

    n_columns = util.MAX_MEM_BLOCK // (
        np.prod(stft_matrix.shape[:-1]) * stft_matrix.itemsize
    )
    n_columns = max(n_columns, 1)
    
    frame = 0
    for bl_s in range(start_frame, n_frames, n_columns):
        
        bl_t = min(bl_s + n_columns, n_frames)

        # invert the block and apply the window function
        ytmp = ifft_window * fft.irfft(stft_matrix[..., bl_s:bl_t], n=n_fft, axis=-2)

        # Overlap-add the istft block starting at the i'th frame
        __overlap_add(y[..., frame * hop_length + offset:], ytmp, hop_length)

        frame += bl_t - bl_s

    # Normalize by sum of squared window
    ifft_window_sum = window_sumsquare(
        window,
        n_frames,
        win_length=win_length,
        n_fft=n_fft,
        hop_length=hop_length,
        dtype=dtype,
    )
    
    # Trim the window normalizer to match the signal length
    if center:
        start = n_fft//2
    else:
        start = 0
    ifft_window_sum = util.fix_length(ifft_window_sum[..., start:], y.shape[-1])

    # Only divide out if it's stable
    approx_nonzero_indices = ifft_window_sum > util.tiny(ifft_window_sum)
    y[..., approx_nonzero_indices] /= ifft_window_sum[approx_nonzero_indices]
    
    return y

@jit(nopython=True)
def __overlap_add(y, ytmp, hop_length):
    # numba-accelerated overlap add for inverse stft
    # y is the pre-allocated output buffer
    # ytmp is the windowed inverse-stft frames
    # hop_length is the hop-length of the STFT analysis

    n_fft = ytmp.shape[-2]
    N = n_fft
    for frame in range(ytmp.shape[-1]):
        sample = frame * hop_length
        if N > y.shape[-1] - sample:
            N = y.shape[-1] - sample
        
        y[..., sample : (sample + N)] += ytmp[..., :N, frame]
```

</details>

---
## Benchmarking 

The effects are less dramatic here compared to the forward transform.  This is because we never had to invoke a copy (eg through padding), and slicing creates views rather than new arrays.  A side effect of the old implementation is that we leak about one frame of memory when slicing, so precise output allocation is a small efficiency gain anyway.

The following are using the same parameters as in the forward transform benchmarks above (5 minutes, 22KHz rate, float64 type; standard STFT parameters).  

```python
>>> %timeit librosa.istft(D, hop_length=hop_length, n_fft=n_fft, center=True, length=None)
447 ms ± 3.89 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

>>> %memit librosa.istft(D, hop_length=hop_length, n_fft=n_fft, center=True, length=None)
peak memory: 1316.61 MiB, increment: 151.45 MiB

>>> %timeit istft(D, hop_length=hop_length, n_fft=n_fft, center=True, length=None)
462 ms ± 13.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

>>> %memit istft(D, hop_length=hop_length, n_fft=n_fft, center=True, length=None)
peak memory: 1336.09 MiB, increment: 170.93 MiB

>>> %timeit istft(D, hop_length=hop_length, n_fft=n_fft, center=True, out=out, length=None)
427 ms ± 11.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

>>> %memit istft(D, hop_length=hop_length, n_fft=n_fft, center=True, out=out, length=None)
peak memory: 1347.66 MiB, increment: 132.04 MiB
```

If we also force the length, the results are about the same:

```python
>>> %timeit librosa.istft(D, hop_length=hop_length, n_fft=n_fft, center=True, length=len(y))
460 ms ± 10.8 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

>>> %memit librosa.istft(D, hop_length=hop_length, n_fft=n_fft, center=True, length=len(y))
peak memory: 1329.12 MiB, increment: 163.96 MiB

>>> %timeit istft(D, hop_length=hop_length, n_fft=n_fft, center=True, length=len(y))
468 ms ± 8.67 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

>>> %memit istft(D, hop_length=hop_length, n_fft=n_fft, center=True, length=len(y))
peak memory: 1363.42 MiB, increment: 198.26 MiB

>>> %timeit istft(D, hop_length=hop_length, n_fft=n_fft, center=True, out=out, length=len(y))
424 ms ± 6.82 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

>>> %memit istft(D, hop_length=hop_length, n_fft=n_fft, center=True, out=out, length=len(y))
peak memory: 1302.14 MiB, increment: 86.52 MiB
```

Results are verified to match numerically against the prior implementation under a variety of conditions:

```python
for N in [2048, 10000]:
    y = np.random.randn(N)
    for n_fft in [1023, 1024, 1025]:
        for hop_length in [129, 255, 256, 257, 384]:
            for center in [False, True]:
                for length in [None, len(y), len(y)//2]:
                    print(N, n_fft, hop_length, center, length)
                    D = librosa.stft(y, hop_length=hop_length, n_fft=n_fft, center=center)
                    yi1 = librosa.istft(D, hop_length=hop_length, n_fft=n_fft, center=center, length=length)
                    yi2 = istft(D, hop_length=hop_length, n_fft=n_fft, center=center, length=length)
                    assert np.allclose(yi1, yi2)
                    out = np.empty_like(yi2)
                    yi3 = istft(D, hop_length=hop_length, n_fft=n_fft, center=center, out=out, length=length)
                    assert yi3 is out
                    assert np.allclose(yi2, yi3)
```

Note the use of `empty_like` instead of `zeros_like` here so that we verify proper buffer initialization.

---
I think this is all ready to go for a PR.

A couple of other things we should investigate:

1. We might have to refactor the griffinlim implementation to make effective use of this feature.  It shouldn't be too difficult, but it's also not trivial because we need to maintain the previous iterate for momentum calculations.
2. Ditto for CQT/VQT.  We could probably get some savings by using one scratch buffer for the intermediate STFT results.  Doing this right may require factoring out some functionality to do exact length calculations for stft sizes.  These may not be the same for each octave, so the best thing to do is probably allocate one array that will fit all octaves, and then provide sliced views as the target output array.   This could get a little delicate.
3. As a separate issue, we should investigate all uses of `np.pad` to see if they could be replaced by more efficient boundary condition handling.  Some good candidates for this are:
    - localmin and localmax  (Probably easy)
    - piptrack  (Less easy)
    - yin and pyin (Probably difficult)
    - feature.rms and zero_crossings (low priority)

Building up the PR, ran into an edge case that we've lost support for here.  If the input signal is shorter than the frame length, padding used to automatically cover this case (for center=True).  Without padding, framing no longer works and we fail.

~~The fix here is to detect if x is shorter than the frame length, and right-pad it (or fix_length it) in advance of framing if we're going to do centered analysis.  This might do some strange things to phase though, and we'll have to test it thoroughly.~~

*EDIT*: The situation is slightly more complex than I originally realized.  Depending on the relationship between hop and frame length, we might have a situation where a frame depends on both head and tail padding, so the two cannot be handled indpendently as my above implementation does.  I think this means that the head portion will need to be tail-padded as well; this might be doable by tail-padding the entire signal up front before head-padding, and then being clever about bookkeeping to avoid extra tail-padding.
Punting this issue up to 0.9.1 to give more time to iron out the details.
Looking back over this, I suspect the simplest solution here is to fall back on a copy-pad (ie our current approach) when the signal is "short".  In this case, "short" means that the combination of frame and hop lengths is such that a frame can span both head and tail padding.  Copying is likely to be cheap in these scenarios, as this corresponds to a short signal (assuming reasonable framing parameters), so it wouldn't be much of a hit.

Once I have a few spare brain cells to rub together, I'll try to get this one bundled up.
:brain: :cloud: :brain:

<details>

<summary>
Updated code for pre-allocating the output buffer and minimal pad-copying:
</summary>

```python
def stft(
    y,
    n_fft=2048,
    hop_length=None,
    win_length=None,
    window="hann",
    center=True,
    dtype=None,
    pad_mode="constant",
    out=None,
):

    # By default, use the entire frame
    if win_length is None:
        win_length = n_fft

    # Set the default hop, if it's not already specified
    if hop_length is None:
        hop_length = int(win_length // 4)

    # Check audio is valid
    util.valid_audio(y, mono=False)

    fft_window = get_window(window, win_length, fftbins=True)

    # Pad the window out to n_fft size
    fft_window = util.pad_center(fft_window, size=n_fft)

    # Reshape so that the window can be broadcast
    fft_window = util.expand_to(fft_window, ndim=1 + y.ndim, axes=-2)

    # Pad the time series so that frames are centered
    
    if center:
        
        if n_fft > y.shape[-1]:
            warnings.warn(
                "n_fft={} is too large for input signal of length={}".format(
                    n_fft, y.shape[-1]
                )
            )

        # Set up the padding array to be empty, and we'll fix the target dimension later
        padding = [(0, 0) for _ in range(y.ndim)]
        
        # How many frames depend on left padding?
        start_k = int(np.ceil(n_fft // 2 / hop_length))

        # What's the first frame that depends on extra right-padding?
        tail_k = (y.shape[-1] + n_fft//2 - n_fft) // hop_length + 1

        if tail_k <= start_k:
            # If tail and head overlap, then just copy-pad the signal and carry on
            start = 0
            extra = 0
            padding[-1] = (n_fft // 2, n_fft // 2)
            y = np.pad(y, padding, mode=pad_mode)
        else:
            # If tail and head do not overlap, then we can implement padding on each part separately
            # and avoid a full copy-pad
            
            # "Middle" of the signal starts here, and does not depend on head padding
            start = start_k * hop_length - n_fft//2
            padding[-1] = (n_fft // 2, 0)

            y_pre = np.pad(y[..., :(start_k - 1) * hop_length - n_fft//2 + n_fft], padding, mode=pad_mode)
            y_frames_pre = util.frame(y_pre, frame_length=n_fft, hop_length=hop_length)
            
            # How many extra frames do we have from the head?
            extra = y_frames_pre.shape[-1]
            
            # Determine if we have any frames that will fit inside the tail pad
            if tail_k * hop_length - n_fft // 2 + n_fft <= y.shape[-1] + n_fft // 2:
            
                padding[-1] = (0, n_fft//2)
                y_post = np.pad(y[..., (tail_k)*hop_length-n_fft//2:], padding, mode=pad_mode)
                y_frames_post = util.frame(y_post, frame_length=n_fft, hop_length=hop_length)
                # How many extra frames do we have from the tail?
                extra += y_frames_post.shape[-1]
            else:
                post_shape = list(y_frames_pre.shape)
                post_shape[-1] = 0
                y_frames_post = np.empty_like(y_frames_pre, shape=post_shape)
            
    else:
        
        if n_fft > y.shape[-1]:
            raise ParameterError(
                "n_fft={} is too large for uncentered analysis of input signal of length={}".format(
                    n_fft, y.shape[-1]
                )
            )
            
        # "Middle" of the signal starts at sample 0
        start = 0
        # We have no extra frames
        extra = 0

    fft = get_fftlib()

    if dtype is None:
        dtype = util.dtype_r2c(y.dtype)
    
    # Window the time series.
    y_frames = util.frame(y[..., start:], frame_length=n_fft, hop_length=hop_length)

    # Pre-allocate the STFT matrix
    shape = list(y_frames.shape)
    
    # This is our frequency dimension
    shape[-2] = 1 + n_fft // 2
    
    # If there's padding, there will be extra head and tail frames
    shape[-1] += extra
        
    if out is None:
        stft_matrix = np.zeros(shape, dtype=dtype, order="F")
    elif not np.allclose(out.shape, shape):
        raise ParameterError(f'Shape mismatch for provided output array out.shape={out.shape} != {shape}')
    else:
        stft_matrix = out

    # Fill in the warm-up
    if center and extra > 0:
        off_start = y_frames_pre.shape[-1]
        stft_matrix[..., :off_start] = fft.rfft(fft_window * y_frames_pre, axis=-2)
        
        off_end = y_frames_post.shape[-1]
        if off_end > 0:
            stft_matrix[..., -off_end:] = fft.rfft(fft_window * y_frames_post, axis=-2)
    else:
        off_start = 0

    n_columns = util.MAX_MEM_BLOCK // (
        np.prod(y_frames.shape[:-1]) * y_frames.itemsize
    )
    n_columns = max(n_columns, 1)

    for bl_s in range(0, y_frames.shape[-1], n_columns):
        bl_t = min(bl_s + n_columns, y_frames.shape[-1])

        stft_matrix[..., bl_s+off_start:bl_t+off_start] = fft.rfft(
            fft_window * y_frames[..., bl_s:bl_t], axis=-2
        )
    return stft_matrix
```
</details>

Revising the benchmarks, since the logic is now a bit more complex than in the previous version due to more sophisticated edge case handling.

```python
sr = 22050
y = np.random.randn(5 * 60 * sr).astype(np.float64)

n_fft = 2048
hop_length = 512
center=False
D1 = librosa.stft(y, center=center, hop_length=hop_length, n_fft=n_fft)
D2 = stft(y, center=center, hop_length=hop_length, n_fft=n_fft)

out = np.empty_like(D2)
D3 = stft(y, center=center, hop_length=hop_length, n_fft=n_fft, out=out)

assert np.allclose(D1, D2)
assert np.allclose(D1, D3)

# Current implementation
>>> %timeit librosa.stft(y, center=center, hop_length=hop_length, n_fft=n_fft)
353 ms ± 17.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

# Proposed minimal copying, without pre-allocation
>>> %timeit stft(y, center=center, hop_length=hop_length, n_fft=n_fft)
246 ms ± 2.54 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

# Proposed with pre-allocation
>>> %timeit stft(y, center=center, hop_length=hop_length, n_fft=n_fft, out=out)
171 ms ± 1.42 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

# Memory benchmarks for the above
>>> %memit librosa.stft(y, center=center, hop_length=hop_length, n_fft=n_fft)
peak memory: 1092.39 MiB, increment: 191.20 MiB

>>> %memit  stft(y, center=center, hop_length=hop_length, n_fft=n_fft)
peak memory: 1069.31 MiB, increment: 168.37 MiB

>>> %memit  stft(y, center=center, hop_length=hop_length, n_fft=n_fft, out=out)
peak memory: 900.96 MiB, increment: 0.00 MiB
```

Correctness check:
```python
for center in [False, True]:
    for N in [512, 1024, 2048, 10000]:
        y = np.random.randn(N)
        for n_fft in [1023, 1024, 1025]:
            if (not center) and N < n_fft:
                continue
            for hop_length in [129, 255, 256, 257, 384, n_fft]:
                print(N, n_fft, hop_length, center)
                D1 = librosa.stft(y, hop_length=hop_length, n_fft=n_fft, center=center)
                D2 = stft(y, hop_length=hop_length, n_fft=n_fft, center=center)
                assert np.allclose(D1, D2)
```
all pass.  These parameters are designed to cover cases where a frame can span head and tail padding, or where the tail padding is not enough to capture any specified frames (this tripped me up before).

I think this one is solid now.  Time to get back to ISTFT.
Just kidding, one more benchmark, just to verify that multichannel works as well (it does).  Here's a 5-minute stereo noise signal at float32 with center-padding (fairly standard use-case).  Savings are tidy:

```python
sr = 22050
y = np.random.randn(2, 5 * 60 * sr).astype(np.float32)n_fft = 2048
hop_length = 512
center = True

D1 = librosa.stft(y, center=center, hop_length=hop_length, n_fft=n_fft)
D2 = stft(y, center=center, hop_length=hop_length, n_fft=n_fft)

out = np.empty_like(D2)
D3 = stft(y, center=center, hop_length=hop_length, n_fft=n_fft, out=out)

assert np.allclose(D1, D2)
assert np.allclose(D1, D3)

# Current implementation
>>> %timeit librosa.stft(y, center=center, hop_length=hop_length, n_fft=n_fft)
627 ms ± 36 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

# Proposed minimal copying, without pre-allocation
>>> %timeit stft(y, center=center, hop_length=hop_length, n_fft=n_fft)
467 ms ± 30.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

# Proposed with pre-allocation
>>> %timeit stft(y, center=center, hop_length=hop_length, n_fft=n_fft, out=out)
382 ms ± 6.45 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

# Memory benchmarks for the above
>>> %memit librosa.stft(y, center=center, hop_length=hop_length, n_fft=n_fft)
peak memory: 1138.02 MiB, increment: 250.70 MiB

>>> %memit  stft(y, center=center, hop_length=hop_length, n_fft=n_fft)
peak memory: 1065.84 MiB, increment: 179.12 MiB

>>> %memit  stft(y, center=center, hop_length=hop_length, n_fft=n_fft, out=out)
peak memory: 886.72 MiB, increment: 0.00 MiB
```
Okay, finally shook out the bugs in istft:

<details>

<summary> Minimal-copy / pre-allocated istft </summary>

```python
def istft(
    stft_matrix,
    hop_length=None,
    win_length=None,
    n_fft=None,
    window="hann",
    center=True,
    dtype=None,
    length=None,
    out=None,
):

    if n_fft is None:
        n_fft = 2 * (stft_matrix.shape[-2] - 1)

    # By default, use the entire frame
    if win_length is None:
        win_length = n_fft

    # Set the default hop, if it's not already specified
    if hop_length is None:
        hop_length = int(win_length // 4)

    ifft_window = get_window(window, win_length, fftbins=True)

    # Pad out to match n_fft, and add broadcasting axes
    ifft_window = util.pad_center(ifft_window, size=n_fft)
    ifft_window = util.expand_to(ifft_window, ndim=stft_matrix.ndim, axes=-2)

    # For efficiency, trim STFT frames according to signal length if available
    if length:
        if center:
            padded_length = length + 2 * (n_fft//2)
        else:
            padded_length = length
        n_frames = min(stft_matrix.shape[-1], int(np.ceil(padded_length / hop_length)))
    else:
        n_frames = stft_matrix.shape[-1]

    if dtype is None:
        dtype = util.dtype_c2r(stft_matrix.dtype)

    shape = list(stft_matrix.shape[:-2])
    expected_signal_len = n_fft + hop_length * (n_frames - 1)
    
    if length:
        expected_signal_len = length
    elif center:
        expected_signal_len -= 2*(n_fft//2)
    
    shape.append(expected_signal_len)
    
    if out is None:
        y = np.zeros(shape, dtype=dtype)
    elif not np.allclose(out.shape, shape):
        raise ParameterError(f'Shape mismatch for provided output array out.shape={out.shape} != {shape}')
    else:
        y = out
        # Since we'll be doing overlap-add here, this needs to be initialized to zero.
        y.fill(0.)

    fft = get_fftlib()

    if center:
        # First frame that does not depend on padding
        #  k * hop_length - n_fft//2 >= 0
        # k * hop_length >= n_fft // 2
        # k >= (n_fft//2 / hop_length)
        
        start_frame = int(np.ceil((n_fft//2) / hop_length))
        
        # Do overlap-add on the head block
        ytmp = ifft_window * fft.irfft(stft_matrix[..., :start_frame], n=n_fft, axis=-2)
        
        shape[-1] = n_fft + hop_length * (start_frame - 1)
        head_buffer = np.zeros(shape, dtype=dtype)
        
        __overlap_add(head_buffer, ytmp, hop_length)
        
        # If y is smaller than the head buffer, take everything
        if y.shape[-1] < shape[-1] - n_fft//2:
            y[..., :] = head_buffer[..., n_fft//2:y.shape[-1]+n_fft//2]
        else:
            # Trim off the first n_fft//2 samples from the head and copy into target buffer
            y[..., :shape[-1]-n_fft//2] = head_buffer[..., n_fft//2:]
        
        # This offset compensates for any differences between frame alignment
        # and padding truncation
        offset = start_frame * hop_length - n_fft//2
        
    else:
        start_frame = 0
        offset = 0

    n_columns = util.MAX_MEM_BLOCK // (
        np.prod(stft_matrix.shape[:-1]) * stft_matrix.itemsize
    )
    n_columns = max(n_columns, 1)
    
    frame = 0
    for bl_s in range(start_frame, n_frames, n_columns):
        
        bl_t = min(bl_s + n_columns, n_frames)

        # invert the block and apply the window function
        ytmp = ifft_window * fft.irfft(stft_matrix[..., bl_s:bl_t], n=n_fft, axis=-2)

        # Overlap-add the istft block starting at the i'th frame
        __overlap_add(y[..., frame * hop_length + offset:], ytmp, hop_length)

        frame += bl_t - bl_s

    # Normalize by sum of squared window
    ifft_window_sum = window_sumsquare(
        window=window,
        n_frames=n_frames,
        win_length=win_length,
        n_fft=n_fft,
        hop_length=hop_length,
        dtype=dtype,
    )
    
    if center:
        start = n_fft//2
    else:
        start = 0
           
    ifft_window_sum = util.fix_length(ifft_window_sum[..., start:], size=y.shape[-1])

    approx_nonzero_indices = ifft_window_sum > util.tiny(ifft_window_sum)
    
    y[..., approx_nonzero_indices] /= ifft_window_sum[approx_nonzero_indices]
    
    return y

@jit(nopython=True)
def __overlap_add(y, ytmp, hop_length):
    # numba-accelerated overlap add for inverse stft
    # y is the pre-allocated output buffer
    # ytmp is the windowed inverse-stft frames
    # hop_length is the hop-length of the STFT analysis

    n_fft = ytmp.shape[-2]
    N = n_fft
    for frame in range(ytmp.shape[-1]):
        sample = frame * hop_length
        if N > y.shape[-1] - sample:
            N = y.shape[-1] - sample
        
        y[..., sample : (sample + N)] += ytmp[..., :N, frame]
```

</details>

The savings are ... non-existent.  For a similar benchmark to the above, we get about 20ms and 5-10MB reduction in footprint.  So, not nothing, but maybe not worth all the effort?

It does pass the following battery of tests though:

```python
for center in [True, False]:
    for N in [512, 1024, 2048, 10000]:
        y = np.random.randn(2, N)
        for n_fft in [1023, 1024, 1025]:
            if (not center) and N < n_fft:
                continue
            for hop_length in [129, 255, 256, 257, 384, n_fft]:
                for length in [None, y.shape[-1], y.shape[-1]//2]:
                    print(N, n_fft, hop_length, center, length)
                    D = librosa.stft(y, hop_length=hop_length, n_fft=n_fft, center=center)
                    yi1 = librosa.istft(D, hop_length=hop_length, n_fft=n_fft, center=center, length=length)
                    yi2 = istft(D, hop_length=hop_length, n_fft=n_fft, center=center, length=length)
                    assert np.allclose(yi1, yi2)
                    out = np.empty_like(yi2)
                    yi3 = istft(D, hop_length=hop_length, n_fft=n_fft, center=center, out=out, length=length)
                    assert yi3 is out
                    assert np.allclose(yi2, yi3)
```

Resolving patch:
diff --git a/docs/examples/plot_pcen_stream.py b/docs/examples/plot_pcen_stream.py
--- a/docs/examples/plot_pcen_stream.py
+++ b/docs/examples/plot_pcen_stream.py
@@ -11,6 +11,10 @@
 
 This is useful when processing long audio files that are too large to load all at
 once, or when streaming data from a recording device.
+
+It also illustrates how to use a pre-allocated output buffer for block-wise
+short-time Fourier transforms.  This provides a minor speed boost and reduction
+in memory usage when processing audio streams.
 """
 
 ##################################################
@@ -59,10 +63,15 @@
 # Initialize the PCEN filter delays to steady state
 zi = None
 
+# Create a handle for storing the block STFT outputs
+# After the first block has been processed, we can re-use
+# this buffer instead of allocating a new one for each block.
+D = None
+
 for y_block in stream:
     # Compute the STFT (without padding, so center=False)
     D = librosa.stft(y_block, n_fft=n_fft, hop_length=hop_length,
-                     center=False)
+                     center=False, out=D)
 
     # Compute PCEN on the magnitude spectrum, using initial delays
     # returned from our previous call (if any)
diff --git a/librosa/core/spectrum.py b/librosa/core/spectrum.py
--- a/librosa/core/spectrum.py
+++ b/librosa/core/spectrum.py
@@ -49,6 +49,7 @@ def stft(
     center=True,
     dtype=None,
     pad_mode="constant",
+    out=None,
 ):
     """Short-time Fourier transform (STFT).
 
@@ -138,16 +139,38 @@ def stft(
         If ``center=True``, this argument is passed to `np.pad` for padding
         the edges of the signal ``y``. By default (``pad_mode="constant"``),
         ``y`` is padded on both sides with zeros.
+
+        .. note:: Not all padding modes supported by `numpy.pad` are supported here.
+            `wrap`, `mean`, `maximum`, `median`, and `minimum` are not supported.
+
+            Other modes that depend at most on input values at the edges of the
+            signal (e.g., `constant`, `edge`, `linear_ramp`) are supported.
+
         If ``center=False``,  this argument is ignored.
 
         .. see also:: `numpy.pad`
 
+    out : np.ndarray or None
+        A pre-allocated, complex-valued array to store the STFT results.
+        This must be of compatible shape and dtype for the given input parameters.
+
+        If `out` is larger than necessary for the provided input signal, then only
+        a prefix slice of `out` will be used.
+
+        If not provided, a new array is allocated and returned.
+
     Returns
     -------
     D : np.ndarray [shape=(..., 1 + n_fft/2, n_frames), dtype=dtype]
         Complex-valued matrix of short-term Fourier transform
         coefficients.
 
+        If a pre-allocated `out` array is provided, then `D` will be
+        a reference to `out`.
+
+        If `out` is larger than necessary, then `D` will be a sliced
+        view: `D = out[..., :n_frames]`.
+
     See Also
     --------
     istft : Inverse STFT
@@ -195,6 +218,8 @@ def stft(
     # Set the default hop, if it's not already specified
     if hop_length is None:
         hop_length = int(win_length // 4)
+    elif not (np.issubdtype(type(hop_length), np.integer) and hop_length > 0):
+        raise ParameterError("hop_length={} must be a positive integer".format(hop_length))
 
     # Check audio is valid
     util.valid_audio(y, mono=False)
@@ -209,47 +234,141 @@ def stft(
 
     # Pad the time series so that frames are centered
     if center:
+        if pad_mode in ("wrap", "maximum", "mean", "median", "minimum"):
+            # Note: padding with a user-provided function "works", but
+            # use at your own risk.
+            # Since we don't pass-through kwargs here, any arguments
+            # to a user-provided pad function should be encapsulated
+            # by using functools.partial:
+            #
+            # >>> my_pad_func = functools.partial(pad_func, foo=x, bar=y)
+            # >>> librosa.stft(..., pad_mode=my_pad_func)
+
+            raise ParameterError("pad_mode='{}' is not supported by librosa.stft".format(pad_mode))
+
         if n_fft > y.shape[-1]:
             warnings.warn(
-                "n_fft={} is too small for input signal of length={}".format(
+                "n_fft={} is too large for input signal of length={}".format(
                     n_fft, y.shape[-1]
-                ),
-                stacklevel=2,
+                )
             )
 
+        # Set up the padding array to be empty, and we'll fix the target dimension later
         padding = [(0, 0) for _ in range(y.ndim)]
-        padding[-1] = (int(n_fft // 2), int(n_fft // 2))
-        y = np.pad(y, padding, mode=pad_mode)
 
-    elif n_fft > y.shape[-1]:
-        raise ParameterError(
-            "n_fft={} is too large for input signal of length={}".format(
-                n_fft, y.shape[-1]
+        # How many frames depend on left padding?
+        start_k = int(np.ceil(n_fft // 2 / hop_length))
+
+        # What's the first frame that depends on extra right-padding?
+        tail_k = (y.shape[-1] + n_fft // 2 - n_fft) // hop_length + 1
+
+        if tail_k <= start_k:
+            # If tail and head overlap, then just copy-pad the signal and carry on
+            start = 0
+            extra = 0
+            padding[-1] = (n_fft // 2, n_fft // 2)
+            y = np.pad(y, padding, mode=pad_mode)
+        else:
+            # If tail and head do not overlap, then we can implement padding on each part separately
+            # and avoid a full copy-pad
+
+            # "Middle" of the signal starts here, and does not depend on head padding
+            start = start_k * hop_length - n_fft // 2
+            padding[-1] = (n_fft // 2, 0)
+
+            y_pre = np.pad(
+                y[..., : (start_k - 1) * hop_length - n_fft // 2 + n_fft],
+                padding,
+                mode=pad_mode,
             )
-        )
+            y_frames_pre = util.frame(y_pre, frame_length=n_fft, hop_length=hop_length)
 
-    # Window the time series.
-    y_frames = util.frame(y, frame_length=n_fft, hop_length=hop_length)
+            # How many extra frames do we have from the head?
+            extra = y_frames_pre.shape[-1]
+
+            # Determine if we have any frames that will fit inside the tail pad
+            if tail_k * hop_length - n_fft // 2 + n_fft <= y.shape[-1] + n_fft // 2:
+                padding[-1] = (0, n_fft // 2)
+                y_post = np.pad(
+                    y[..., (tail_k) * hop_length - n_fft // 2 :], padding, mode=pad_mode
+                )
+                y_frames_post = util.frame(
+                    y_post, frame_length=n_fft, hop_length=hop_length
+                )
+                # How many extra frames do we have from the tail?
+                extra += y_frames_post.shape[-1]
+            else:
+                # In this event, the first frame that touches tail padding would run off
+                # the end of the padded array
+                # We'll circumvent this by allocating an empty frame buffer for the tail
+                # this keeps the subsequent logic simple
+                post_shape = list(y_frames_pre.shape)
+                post_shape[-1] = 0
+                y_frames_post = np.empty_like(y_frames_pre, shape=post_shape)
+    else:
+        if n_fft > y.shape[-1]:
+            raise ParameterError(
+                "n_fft={} is too large for uncentered analysis of input signal of length={}".format(
+                    n_fft, y.shape[-1]
+                )
+            )
+
+        # "Middle" of the signal starts at sample 0
+        start = 0
+        # We have no extra frames
+        extra = 0
 
     fft = get_fftlib()
 
     if dtype is None:
         dtype = util.dtype_r2c(y.dtype)
 
+    # Window the time series.
+    y_frames = util.frame(y[..., start:], frame_length=n_fft, hop_length=hop_length)
+
     # Pre-allocate the STFT matrix
     shape = list(y_frames.shape)
+
+    # This is our frequency dimension
     shape[-2] = 1 + n_fft // 2
-    stft_matrix = np.empty(shape, dtype=dtype, order="F")
 
-    n_columns = util.MAX_MEM_BLOCK // (
-        np.prod(stft_matrix.shape[:-1]) * stft_matrix.itemsize
-    )
+    # If there's padding, there will be extra head and tail frames
+    shape[-1] += extra
+
+    if out is None:
+        stft_matrix = np.zeros(shape, dtype=dtype, order="F")
+    elif not (np.allclose(out.shape[:-1], shape[:-1]) and out.shape[-1] >= shape[-1]):
+        raise ParameterError(
+            f"Shape mismatch for provided output array out.shape={out.shape} and target shape={shape}"
+        )
+    elif not np.iscomplexobj(out):
+        raise ParameterError(
+            f"output with dtype={out.dtype} is not of complex type"
+        )
+    else:
+        if np.allclose(shape, out.shape):
+            stft_matrix = out
+        else:
+            stft_matrix = out[..., :shape[-1]]
+
+    # Fill in the warm-up
+    if center and extra > 0:
+        off_start = y_frames_pre.shape[-1]
+        stft_matrix[..., :off_start] = fft.rfft(fft_window * y_frames_pre, axis=-2)
+
+        off_end = y_frames_post.shape[-1]
+        if off_end > 0:
+            stft_matrix[..., -off_end:] = fft.rfft(fft_window * y_frames_post, axis=-2)
+    else:
+        off_start = 0
+
+    n_columns = util.MAX_MEM_BLOCK // (np.prod(y_frames.shape[:-1]) * y_frames.itemsize)
     n_columns = max(n_columns, 1)
 
-    for bl_s in range(0, stft_matrix.shape[-1], n_columns):
-        bl_t = min(bl_s + n_columns, stft_matrix.shape[-1])
+    for bl_s in range(0, y_frames.shape[-1], n_columns):
+        bl_t = min(bl_s + n_columns, y_frames.shape[-1])
 
-        stft_matrix[..., bl_s:bl_t] = fft.rfft(
+        stft_matrix[..., bl_s + off_start : bl_t + off_start] = fft.rfft(
             fft_window * y_frames[..., bl_s:bl_t], axis=-2
         )
     return stft_matrix
@@ -266,6 +385,7 @@ def istft(
     center=True,
     dtype=None,
     length=None,
+    out=None,
 ):
     """
     Inverse short-time Fourier transform (ISTFT).
@@ -324,6 +444,12 @@ def istft(
         If provided, the output ``y`` is zero-padded or clipped to exactly
         ``length`` samples.
 
+    out : np.ndarray or None
+        A pre-allocated, complex-valued array to store the reconstructed signal
+        ``y``.  This must be of the correct shape for the given input parameters.
+
+        If not provided, a new array is allocated and returned.
+
     Returns
     -------
     y : np.ndarray [shape=(..., n)]
@@ -380,7 +506,7 @@ def istft(
     # For efficiency, trim STFT frames according to signal length if available
     if length:
         if center:
-            padded_length = length + int(n_fft)
+            padded_length = length + 2 * (n_fft//2)
         else:
             padded_length = length
         n_frames = min(stft_matrix.shape[-1], int(np.ceil(padded_length / hop_length)))
@@ -392,25 +518,71 @@ def istft(
 
     shape = list(stft_matrix.shape[:-2])
     expected_signal_len = n_fft + hop_length * (n_frames - 1)
+
+    if length:
+        expected_signal_len = length
+    elif center:
+        expected_signal_len -= 2*(n_fft//2)
+
     shape.append(expected_signal_len)
-    y = np.zeros(shape, dtype=dtype)
+
+    if out is None:
+        y = np.zeros(shape, dtype=dtype)
+    elif not np.allclose(out.shape, shape):
+        raise ParameterError(f'Shape mismatch for provided output array out.shape={out.shape} != {shape}')
+    else:
+        y = out
+        # Since we'll be doing overlap-add here, this needs to be initialized to zero.
+        y.fill(0.)
+
+    fft = get_fftlib()
+
+    if center:
+        # First frame that does not depend on padding
+        #  k * hop_length - n_fft//2 >= 0
+        # k * hop_length >= n_fft // 2
+        # k >= (n_fft//2 / hop_length)
+
+        start_frame = int(np.ceil((n_fft//2) / hop_length))
+
+        # Do overlap-add on the head block
+        ytmp = ifft_window * fft.irfft(stft_matrix[..., :start_frame], n=n_fft, axis=-2)
+
+        shape[-1] = n_fft + hop_length * (start_frame - 1)
+        head_buffer = np.zeros(shape, dtype=dtype)
+
+        __overlap_add(head_buffer, ytmp, hop_length)
+
+        # If y is smaller than the head buffer, take everything
+        if y.shape[-1] < shape[-1] - n_fft//2:
+            y[..., :] = head_buffer[..., n_fft//2:y.shape[-1]+n_fft//2]
+        else:
+            # Trim off the first n_fft//2 samples from the head and copy into target buffer
+            y[..., :shape[-1]-n_fft//2] = head_buffer[..., n_fft//2:]
+
+        # This offset compensates for any differences between frame alignment
+        # and padding truncation
+        offset = start_frame * hop_length - n_fft//2
+
+    else:
+        start_frame = 0
+        offset = 0
 
     n_columns = util.MAX_MEM_BLOCK // (
         np.prod(stft_matrix.shape[:-1]) * stft_matrix.itemsize
     )
     n_columns = max(n_columns, 1)
 
-    fft = get_fftlib()
-
     frame = 0
-    for bl_s in range(0, n_frames, n_columns):
+    for bl_s in range(start_frame, n_frames, n_columns):
+
         bl_t = min(bl_s + n_columns, n_frames)
 
         # invert the block and apply the window function
         ytmp = ifft_window * fft.irfft(stft_matrix[..., bl_s:bl_t], n=n_fft, axis=-2)
 
         # Overlap-add the istft block starting at the i'th frame
-        __overlap_add(y[..., frame * hop_length :], ytmp, hop_length)
+        __overlap_add(y[..., frame * hop_length + offset:], ytmp, hop_length)
 
         frame += bl_t - bl_s
 
@@ -424,26 +596,16 @@ def istft(
         dtype=dtype,
     )
 
-    approx_nonzero_indices = ifft_window_sum > util.tiny(ifft_window_sum)
-    y[..., approx_nonzero_indices] /= ifft_window_sum[approx_nonzero_indices]
-
-    if length is None:
-        # If we don't need to control length, just do the usual center trimming
-        # to eliminate padded data
-        if center:
-            y = y[..., int(n_fft // 2) : -int(n_fft // 2)]
+    if center:
+        start = n_fft//2
     else:
-        if center:
-            # If we're centering, crop off the first n_fft//2 samples
-            # and then trim/pad to the target length.
-            # We don't trim the end here, so that if the signal is zero-padded
-            # to a longer duration, the decay is smooth by windowing
-            start = int(n_fft // 2)
-        else:
-            # If we're not centering, start at 0 and trim/pad as necessary
-            start = 0
+        start = 0
+
+    ifft_window_sum = util.fix_length(ifft_window_sum[..., start:], size=y.shape[-1])
 
-        y = util.fix_length(y[..., start:], size=length)
+    approx_nonzero_indices = ifft_window_sum > util.tiny(ifft_window_sum)
+
+    y[..., approx_nonzero_indices] /= ifft_window_sum[approx_nonzero_indices]
 
     return y
 
@@ -456,9 +618,13 @@ def __overlap_add(y, ytmp, hop_length):
     # hop_length is the hop-length of the STFT analysis
 
     n_fft = ytmp.shape[-2]
+    N = n_fft
     for frame in range(ytmp.shape[-1]):
         sample = frame * hop_length
-        y[..., sample : (sample + n_fft)] += ytmp[..., frame]
+        if N > y.shape[-1] - sample:
+            N = y.shape[-1] - sample
+
+        y[..., sample : (sample + N)] += ytmp[..., :N, frame]
 
 
 def __reassign_frequencies(
@@ -1070,9 +1236,9 @@ def reassigned_spectrogram(
     # find bins below the power threshold
     # reassigned bins with zero power will already be NaN
     if callable(ref_power):
-        ref_power = ref_power(mags ** 2)
+        ref_power = ref_power(mags**2)
 
-    mags_low = np.less(mags, ref_power ** 0.5, where=~np.isnan(mags))
+    mags_low = np.less(mags, ref_power**0.5, where=~np.isnan(mags))
 
     # for reassigned estimates, optionally set thresholded bins to NaN, return
     # bin frequencies and frame times in place of NaN generated by
@@ -1689,7 +1855,7 @@ def amplitude_to_db(S, *, ref=1.0, amin=1e-5, top_db=80.0):
 
     power = np.square(magnitude, out=magnitude)
 
-    return power_to_db(power, ref=ref_value ** 2, amin=amin ** 2, top_db=top_db)
+    return power_to_db(power, ref=ref_value**2, amin=amin**2, top_db=top_db)
 
 
 @cache(level=30)
@@ -1716,7 +1882,7 @@ def db_to_amplitude(S_db, *, ref=1.0):
     -----
     This function caches at level 30.
     """
-    return db_to_power(S_db, ref=ref ** 2) ** 0.5
+    return db_to_power(S_db, ref=ref**2) ** 0.5
 
 
 @cache(level=30)
@@ -1976,7 +2142,7 @@ def fmt(y, *, t_min=0.5, n_fmt=None, kind="cubic", beta=0.5, over_sample=1, axis
     # Normalization is absorbed into the window here for expedience
     fft = get_fftlib()
     return fft.rfft(
-        y_res * ((x_exp ** beta).reshape(shape) * np.sqrt(n) / n_fmt), axis=axis
+        y_res * ((x_exp**beta).reshape(shape) * np.sqrt(n) / n_fmt), axis=axis
     )
 
 
@@ -2188,7 +2354,7 @@ def pcen(
         # which approximates the full-width half-max of the
         # squared frequency response of the IIR low-pass filter
 
-        b = (np.sqrt(1 + 4 * t_frames ** 2) - 1) / (2 * t_frames ** 2)
+        b = (np.sqrt(1 + 4 * t_frames**2) - 1) / (2 * t_frames**2)
 
     if not 0 <= b <= 1:
         raise ParameterError("b={} must be between 0 and 1".format(b))
@@ -2241,7 +2407,7 @@ def pcen(
     elif bias == 0:
         S_out = np.exp(power * (np.log(S) + np.log(smooth)))
     else:
-        S_out = (bias ** power) * np.expm1(power * np.log1p(S * smooth / bias))
+        S_out = (bias**power) * np.expm1(power * np.log1p(S * smooth / bias))
 
     if return_zf:
         return S_out, zf
@@ -2409,8 +2575,8 @@ def griffinlim(
     if n_fft is None:
         n_fft = 2 * (S.shape[-2] - 1)
 
-    # using complex64 will keep the result to minimal necessary precision
-    angles = np.empty(S.shape, dtype=np.complex64)
+    # Infer the dtype from S
+    angles = np.empty(S.shape, dtype=util.dtype_r2c(S.dtype))
     eps = util.tiny(angles)
 
     if init == "random":
@@ -2422,16 +2588,17 @@ def griffinlim(
     else:
         raise ParameterError("init={} must either None or 'random'".format(init))
 
-    # And initialize the previous iterate to 0
-    rebuilt = 0.0
+    # Place-holders for temporary data and reconstructed buffer
+    rebuilt = None
+    tprev = None
+    inverse = None
 
+    # Absorb magnitudes into angles
+    angles *= S
     for _ in range(n_iter):
-        # Store the previous iterate
-        tprev = rebuilt
-
         # Invert with our current estimate of the phases
         inverse = istft(
-            S * angles,
+            angles,
             hop_length=hop_length,
             win_length=win_length,
             n_fft=n_fft,
@@ -2439,6 +2606,7 @@ def griffinlim(
             center=center,
             dtype=dtype,
             length=length,
+            out=inverse,
         )
 
         # Rebuild the spectrogram
@@ -2450,15 +2618,21 @@ def griffinlim(
             window=window,
             center=center,
             pad_mode=pad_mode,
+            out=rebuilt
         )
 
         # Update our phase estimates
-        angles[:] = rebuilt - (momentum / (1 + momentum)) * tprev
-        angles[:] /= np.abs(angles) + eps
+        angles[:] = rebuilt
+        if tprev is not None:
+            angles -= (momentum / (1 + momentum)) * tprev
+        angles /= np.abs(angles) + eps
+        angles *= S
+        # Store
+        rebuilt, tprev = tprev, rebuilt
 
     # Return the final phase estimates
     return istft(
-        S * angles,
+        angles,
         hop_length=hop_length,
         win_length=win_length,
         n_fft=n_fft,
@@ -2466,6 +2640,7 @@ def griffinlim(
         center=center,
         dtype=dtype,
         length=length,
+        out=inverse
     )
 
 


Test patch:
diff --git a/tests/test_core.py b/tests/test_core.py
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -72,7 +72,7 @@ def test_load_soundfile():
 
 def test_load_audioread():
     fname = os.path.join("tests", "data", "test1_44100.wav")
-    
+
     # Load using an existing audioread object
     reader = audioread.rawread.RawAudioFile(fname)
     y, sr = librosa.load(reader, sr=None)
@@ -169,7 +169,9 @@ def test_resample_mono(resample_mono, sr_out, res_type, fix):
     y, sr_in = resample_mono
     y = librosa.to_mono(y)
 
-    y2 = librosa.resample(y, orig_sr=sr_in, target_sr=sr_out, res_type=res_type, fix=fix)
+    y2 = librosa.resample(
+        y, orig_sr=sr_in, target_sr=sr_out, res_type=res_type, fix=fix
+    )
 
     # First, check that the audio is valid
     librosa.util.valid_audio(y2, mono=True)
@@ -213,7 +215,9 @@ def test_resample_stereo(resample_audio, sr_out, res_type, fix):
 
     y, sr_in = resample_audio
 
-    y2 = librosa.resample(y, orig_sr=sr_in, target_sr=sr_out, res_type=res_type, fix=fix)
+    y2 = librosa.resample(
+        y, orig_sr=sr_in, target_sr=sr_out, res_type=res_type, fix=fix
+    )
 
     # First, check that the audio is valid
     librosa.util.valid_audio(y2, mono=False)
@@ -250,7 +254,9 @@ def test_resample_scale(resample_mono, res_type, sr_out):
 
     y, sr_in = resample_mono
 
-    y2 = librosa.resample(y, orig_sr=sr_in, target_sr=sr_out, res_type=res_type, scale=True)
+    y2 = librosa.resample(
+        y, orig_sr=sr_in, target_sr=sr_out, res_type=res_type, scale=True
+    )
 
     # First, check that the audio is valid
     librosa.util.valid_audio(y2, mono=True)
@@ -321,14 +327,90 @@ def test_stft_winsizes():
     x = np.zeros(1000000)
 
     for power in range(12, 17):
-        N = 2 ** power
+        N = 2**power
         H = N // 2
         librosa.stft(x, n_fft=N, hop_length=H, win_length=N)
 
 
+@pytest.mark.parametrize("center", [False, True])
+@pytest.mark.parametrize(
+    "n_fft, hop_length",
+    [(1023, 128), (1023, 129), (1023, 256), (2048, 512), (2048, 2048)],
+)
+@pytest.mark.parametrize("N", [1024, 2048, 8192])
+def test_stft_preallocate(center, n_fft, hop_length, N):
+
+    # Work in stereo by default
+    y = np.random.randn(2, max(N, n_fft))
+
+    D1 = librosa.stft(y, center=center, n_fft=n_fft, hop_length=hop_length)
+    out = np.empty_like(D1)
+    D2 = librosa.stft(y, center=center, n_fft=n_fft, hop_length=hop_length, out=out)
+    assert D2 is out
+    assert np.allclose(D1, D2)
+
+
+@pytest.mark.parametrize("center", [False, True])
+@pytest.mark.parametrize(
+    "n_fft, hop_length",
+    [(1023, 128), (1023, 129), (1023, 256), (2048, 512), (2048, 2048)],
+)
+@pytest.mark.parametrize("N", [2048])
+def test_stft_preallocate_oversize(center, n_fft, hop_length, N):
+
+    # Work in stereo by default
+    y = np.random.randn(2, max(N, n_fft))
+
+    D1 = librosa.stft(y, center=center, n_fft=n_fft, hop_length=hop_length)
+    shape = list(D1.shape)
+    shape[-1] *= 2
+    out = np.empty_like(D1, shape=shape)
+    D2 = librosa.stft(y, center=center, n_fft=n_fft, hop_length=hop_length, out=out)
+    assert np.allclose(D1, D2)
+    assert np.allclose(D1, out[..., :D2.shape[-1]])
+
+
+@pytest.mark.parametrize("center", [False, True])
+@pytest.mark.parametrize(
+    "n_fft, hop_length",
+    [(1023, 128), (1023, 129), (1023, 256), (2048, 512), (2048, 2048)],
+)
+@pytest.mark.parametrize("N", [2048])
+@pytest.mark.xfail(raises=librosa.ParameterError)
+def test_stft_preallocate_undersize(center, n_fft, hop_length, N):
+
+    # Work in stereo by default
+    y = np.random.randn(2, max(N, n_fft))
+
+    D1 = librosa.stft(y, center=center, n_fft=n_fft, hop_length=hop_length)
+    shape = list(D1.shape)
+    shape[-1] //= 2
+    out = np.empty_like(D1, shape=shape)
+    D2 = librosa.stft(y, center=center, n_fft=n_fft, hop_length=hop_length, out=out)
+
+
+@pytest.mark.parametrize("center", [False, True])
+@pytest.mark.parametrize(
+    "n_fft, hop_length",
+    [(1023, 128), (1023, 129), (1023, 256), (2048, 512), (2048, 2048)],
+)
+@pytest.mark.parametrize("N", [1024, 2048, 8192])
+def test_istft_preallocate(center, n_fft, hop_length, N):
+    y = np.random.randn(2, max(N, n_fft))
+
+    D = librosa.stft(y, center=center, n_fft=n_fft, hop_length=hop_length)
+
+    y1 = librosa.istft(D, center=center, n_fft=n_fft, hop_length=hop_length)
+    y2 = np.empty_like(y1)
+    y3 = librosa.istft(D, center=center, n_fft=n_fft, hop_length=hop_length, out=y2)
+
+    assert y3 is y2
+    assert np.allclose(y1, y2)
+
+
 # results for FFT bins containing multiple components will be unstable, as when
 # using higher sampling rates or shorter windows with this test signal
-@pytest.mark.parametrize("center", [False, True])
+@pytest.mark.parametrize("center", [False])
 @pytest.mark.parametrize("sr", [256, 512, 2000, 2048])
 @pytest.mark.parametrize("n_fft", [128, 255, 256, 512, 1280])
 def test___reassign_frequencies(sr, n_fft, center):
@@ -336,7 +418,7 @@ def test___reassign_frequencies(sr, n_fft, center):
     y = np.sin(17 * x * 2 * np.pi) + np.sin(103 * x * 2 * np.pi)
 
     freqs, S = librosa.core.spectrum.__reassign_frequencies(
-        y=y, sr=sr, n_fft=n_fft, hop_length=n_fft, center=center, pad_mode="wrap"
+        y=y, sr=sr, n_fft=n_fft, hop_length=n_fft, center=center
     )
 
     S_db = librosa.amplitude_to_db(np.abs(S), ref=np.max)
@@ -605,7 +687,12 @@ def test_salience_basecase():
     harms = [1]
     weights = [1.0]
     S_sal = librosa.core.salience(
-        S, freqs=freqs, harmonics=harms, weights=weights, filter_peaks=False, kind="quadratic"
+        S,
+        freqs=freqs,
+        harmonics=harms,
+        weights=weights,
+        filter_peaks=False,
+        kind="quadratic",
     )
     assert np.allclose(S_sal, S)
 
@@ -617,7 +704,12 @@ def test_salience_basecase2():
     harms = [1, 0.5, 2.0]
     weights = [1.0, 0.0, 0.0]
     S_sal = librosa.core.salience(
-        S, freqs=freqs, harmonics=harms, weights=weights, filter_peaks=False, kind="quadratic"
+        S,
+        freqs=freqs,
+        harmonics=harms,
+        weights=weights,
+        filter_peaks=False,
+        kind="quadratic",
     )
     assert np.allclose(S_sal, S)
 
@@ -626,7 +718,9 @@ def test_salience_defaults():
     S = np.array([[0.1, 0.5, 0.0], [0.2, 1.2, 1.2], [0.0, 0.7, 0.3], [1.3, 3.2, 0.8]])
     freqs = np.array([50.0, 100.0, 200.0, 400.0])
     harms = [0.5, 1, 2]
-    actual = librosa.core.salience(S, freqs=freqs, harmonics=harms, kind="quadratic", fill_value=0.0)
+    actual = librosa.core.salience(
+        S, freqs=freqs, harmonics=harms, kind="quadratic", fill_value=0.0
+    )
 
     expected = (
         np.array([[0.0, 0.0, 0.0], [0.3, 2.4, 1.5], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]])
@@ -641,7 +735,12 @@ def test_salience_weights():
     harms = [0.5, 1, 2]
     weights = [1.0, 1.0, 1.0]
     actual = librosa.core.salience(
-        S, freqs=freqs, harmonics=harms, weights=weights, kind="quadratic", fill_value=0.0
+        S,
+        freqs=freqs,
+        harmonics=harms,
+        weights=weights,
+        kind="quadratic",
+        fill_value=0.0,
     )
 
     expected = (
@@ -657,7 +756,12 @@ def test_salience_no_peak_filter():
     harms = [0.5, 1, 2]
     weights = [1.0, 1.0, 1.0]
     actual = librosa.core.salience(
-        S, freqs=freqs, harmonics=harms, weights=weights, filter_peaks=False, kind="quadratic"
+        S,
+        freqs=freqs,
+        harmonics=harms,
+        weights=weights,
+        filter_peaks=False,
+        kind="quadratic",
     )
 
     expected = (
@@ -673,7 +777,13 @@ def test_salience_aggregate():
     harms = [0.5, 1, 2]
     weights = [1.0, 1.0, 1.0]
     actual = librosa.core.salience(
-        S, freqs=freqs, harmonics=harms, weights=weights, aggregate=np.ma.max, kind="quadratic", fill_value=0.0
+        S,
+        freqs=freqs,
+        harmonics=harms,
+        weights=weights,
+        aggregate=np.ma.max,
+        kind="quadratic",
+        fill_value=0.0,
     )
 
     expected = np.array(
@@ -714,7 +824,7 @@ def test_magphase_zero():
     assert S.dtype is np.dtype("float32")
     assert P.dtype is np.dtype("complex64")
     assert np.allclose(S, 0)
-    assert np.allclose(P, 1+0j)
+    assert np.allclose(P, 1 + 0j)
 
 
 def test_magphase_denormalized():
@@ -725,7 +835,7 @@ def test_magphase_denormalized():
     assert S.dtype is np.dtype("float32")
     assert P.dtype is np.dtype("complex64")
     assert np.allclose(S, 1.0e-42)
-    assert np.allclose(P, 0+1j)
+    assert np.allclose(P, 0 + 1j)
 
 
 def test_magphase_real():
@@ -741,7 +851,7 @@ def test_magphase_real():
             [P[0, 0], P[0, 1] ** 2],  # negative zero can have phase +1 or -1
             [P[1, 0], P[1, 1]],
         ],
-        np.array([[-1+0j, 1+0j], [1+0j, 1+0j]])
+        np.array([[-1 + 0j, 1 + 0j], [1 + 0j, 1 + 0j]]),
     )
 
 
@@ -940,7 +1050,7 @@ def test_to_mono(y):
 
 
 @pytest.mark.parametrize(
-    "y", [np.ones((2, 10)), np.ones((2, 3, 10)), np.ones((2,3,4,10))]
+    "y", [np.ones((2, 10)), np.ones((2, 3, 10)), np.ones((2, 3, 4, 10))]
 )
 def test_to_mono_multi(y):
     y_mono = librosa.to_mono(y)
@@ -1094,10 +1204,10 @@ def test_pyin_multi():
     y = np.stack([librosa.tone(440, duration=1.0), librosa.tone(560, duration=1.0)])
 
     # Taper the signal
-    h = librosa.filters.get_window('triangle', y.shape[-1])
+    h = librosa.filters.get_window("triangle", y.shape[-1])
 
     # Filter it
-    y = y * h[np.newaxis,:]
+    y = y * h[np.newaxis, :]
 
     # Disable nans so we can use allclose checks
     fall, vall, vpall = librosa.pyin(y, fmin=100, fmax=1000, center=False, fill_na=-1)
@@ -1116,13 +1226,15 @@ def test_pyin_multi_center():
     y = np.stack([librosa.tone(440, duration=1.0), librosa.tone(560, duration=1.0)])
 
     # Taper the signal
-    h = librosa.filters.get_window('triangle', y.shape[-1])
+    h = librosa.filters.get_window("triangle", y.shape[-1])
 
     # Filter it
-    y = y * h[np.newaxis,:]
+    y = y * h[np.newaxis, :]
 
     # Disable nans so we can use allclose checks
-    fleft, vleft, vpleft = librosa.pyin(y, fmin=100, fmax=1000, center=False, fill_na=-1)
+    fleft, vleft, vpleft = librosa.pyin(
+        y, fmin=100, fmax=1000, center=False, fill_na=-1
+    )
     fc, vc, vpc = librosa.pyin(y, fmin=100, fmax=1000, center=True, fill_na=-1)
 
     # Centering will pad by half a frame on either side
@@ -1265,7 +1377,7 @@ def test__spectrogram(y_22050, n_fft, hop_length, power):
     # And only the spectrogram but with incorrect n_fft
     S_, n_fft_ = librosa.core.spectrum._spectrogram(S=S, n_fft=2 * n_fft, power=power)
     assert np.allclose(S, S_)
-    
+
     assert np.allclose(2 * (S.shape[-2] - 1), n_fft_)
 
 
@@ -1298,8 +1410,8 @@ def test_power_to_db_fail(x, top_db, amin):
 def test_power_to_db_inv(erp, k):
 
     y_true = (k - erp) * 10
-    x = 10.0 ** k
-    rp = 10.0 ** erp
+    x = 10.0**k
+    rp = 10.0**erp
     y = librosa.power_to_db(x, ref=rp, top_db=None)
 
     assert np.isclose(y, y_true)
@@ -1314,7 +1426,7 @@ def test_amplitude_to_db():
     x = np.abs(np.random.randn(1000)) + NOISE_FLOOR
 
     db1 = librosa.amplitude_to_db(x, top_db=None)
-    db2 = librosa.power_to_db(x ** 2, top_db=None)
+    db2 = librosa.power_to_db(x**2, top_db=None)
 
     assert np.allclose(db1, db2)
 
@@ -1332,7 +1444,7 @@ def test_amplitude_to_db_complex():
         assert len(out) > 0
         assert "complex" in str(out[0].message).lower()
 
-    db2 = librosa.power_to_db(x ** 2, top_db=None)
+    db2 = librosa.power_to_db(x**2, top_db=None)
 
     assert np.allclose(db1, db2)
 
@@ -1341,7 +1453,7 @@ def test_amplitude_to_db_complex():
 @pytest.mark.parametrize("xp", [(np.abs(np.random.randn(1000)) + 1e-5) ** 2])
 def test_db_to_power_inv(ref_p, xp):
 
-    ref = 10.0 ** ref_p
+    ref = 10.0**ref_p
     db = librosa.power_to_db(xp, ref=ref, top_db=None)
     xp2 = librosa.db_to_power(db, ref=ref)
 
@@ -1353,8 +1465,8 @@ def test_db_to_power_inv(ref_p, xp):
 def test_db_to_power(erp, db):
 
     y = db
-    rp = 10.0 ** erp
-    x_true = 10.0 ** erp * (10.0 ** (0.1 * db))
+    rp = 10.0**erp
+    x_true = 10.0**erp * (10.0 ** (0.1 * db))
 
     x = librosa.db_to_power(y, ref=rp)
 
@@ -1365,7 +1477,7 @@ def test_db_to_power(erp, db):
 @pytest.mark.parametrize("xp", [(np.abs(np.random.randn(1000)) + 1e-5)])
 def test_db_to_amplitude_inv(xp, ref_p):
 
-    ref = 10.0 ** ref_p
+    ref = 10.0**ref_p
     db = librosa.amplitude_to_db(xp, ref=ref, top_db=None)
     xp2 = librosa.db_to_amplitude(db, ref=ref)
 
@@ -1541,7 +1653,7 @@ def y_orig():
 def test_fmt_scale(y_orig, y_res, n_fmt, kind, atol, SCALE, OVER_SAMPLE):
 
     # Make sure our signals preserve energy
-    assert np.allclose(np.sum(y_orig ** 2), np.sum(y_res ** 2))
+    assert np.allclose(np.sum(y_orig**2), np.sum(y_res**2))
 
     # Scale-transform the original
     f_orig = librosa.fmt(
@@ -1647,7 +1759,7 @@ def test_harmonics_2d():
 
 
 def test_harmonics_1d_nonunique():
-    x = np.arange(-8, 8)**2
+    x = np.arange(-8, 8) ** 2
     y = np.linspace(-8, 8, num=len(x), endpoint=False) ** 2
 
     h = [0.25, 0.5, 1, 2, 4]
@@ -1837,7 +1949,7 @@ def test_pcen_power(S_pcen, p):
     P = librosa.pcen(
         S_pcen, gain=0, bias=0, power=p, b=1, time_constant=0.5, eps=1e-6, max_size=1
     )
-    assert np.allclose(P, S_pcen ** p)
+    assert np.allclose(P, S_pcen**p)
 
 
 def test_pcen_ones(S_pcen):
@@ -2043,7 +2155,7 @@ def test_reset_fftlib():
 @pytest.fixture
 def y_chirp():
     sr = 22050
-    y = librosa.chirp(fmin=55, fmax=55 * 2 ** 7, length=sr // 8, sr=sr)
+    y = librosa.chirp(fmin=55, fmax=55 * 2**7, length=sr // 8, sr=sr)
     return y
 
 
@@ -2240,7 +2352,9 @@ def test_stream(
         # frame this for easy checking
         y_b_mono = librosa.to_mono(y_block)
         if len(y_b_mono) >= frame_length:
-            y_b_frame = librosa.util.frame(y_b_mono, frame_length=frame_length, hop_length=hop_length)
+            y_b_frame = librosa.util.frame(
+                y_b_mono, frame_length=frame_length, hop_length=hop_length
+            )
             y_frame_stream.append(y_b_frame)
 
     # Concatenate the framed blocks together
@@ -2257,7 +2371,9 @@ def test_stream(
         path, sr=None, dtype=dtype, mono=True, offset=offset, duration=duration
     )
     # First, check the rate
-    y_frame = librosa.util.frame(y_full, frame_length=frame_length, hop_length=hop_length)
+    y_frame = librosa.util.frame(
+        y_full, frame_length=frame_length, hop_length=hop_length
+    )
 
     # Raw audio will not be padded
     n = y_frame.shape[1]
@@ -2334,3 +2450,28 @@ def test_mu_expand_badmu():
 )
 def test_mu_expand_badx(x):
     librosa.mu_expand(x, quantize=False)
+
+
+@pytest.mark.xfail(raises=librosa.ParameterError)
+def test_stft_bad_prealloc_shape():
+    y = np.zeros(22050)
+
+    # Output shape here is incorrect, and should trigger a failure
+    S1 = librosa.stft(
+        y, n_fft=512, hop_length=128, out=np.zeros((100, 10), dtype=np.complex)
+    )
+
+
+@pytest.mark.xfail(raises=librosa.ParameterError)
+def test_stft_bad_prealloc_dtype():
+    y = np.zeros(22050)
+    D = librosa.stft(y)
+
+    Dbad = np.zeros(D.shape, dtype=np.float32)
+    librosa.stft(y, out=Dbad)
+
+
+@pytest.mark.xfail(raises=librosa.ParameterError)
+def test_istft_bad_prealloc_shape():
+    D = np.zeros((1025, 5), dtype=np.complex64)
+    librosa.istft(D, out=np.zeros(100))
diff --git a/tests/test_failures.py b/tests/test_failures.py
--- a/tests/test_failures.py
+++ b/tests/test_failures.py
@@ -131,3 +131,10 @@ def test_istft_bad_window():
     window = np.ones(n_fft // 2)
 
     librosa.istft(D, window=window)
+
+
+@pytest.mark.xfail(raises=librosa.ParameterError)
+@pytest.mark.parametrize('y', [np.empty(22050)])
+@pytest.mark.parametrize('mode', ['wrap', 'maximum', 'minimum', 'median', 'mean'])
+def test_stft_bad_pad(y, mode):
+    librosa.stft(y, pad_mode=mode)