File: spectrogram.py

package info (click to toggle)
python-vispy 0.15.2-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 8,868 kB
  • sloc: python: 59,799; javascript: 6,800; makefile: 69; sh: 6
file content (169 lines) | stat: -rw-r--r-- 4,940 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
# -*- coding: utf-8 -*-
# -----------------------------------------------------------------------------
# Copyright (c) Vispy Development Team. All Rights Reserved.
# Distributed under the (new) BSD License. See LICENSE.txt for more info.
# -----------------------------------------------------------------------------

import numpy as np

from .image import ImageVisual
from ..util.fourier import stft, fft_freqs


class SpectrogramVisual(ImageVisual):
    """Calculate and show a spectrogram

    Parameters
    ----------
    x : array-like
        1D signal to operate on. ``If len(x) < n_fft``, x will be
        zero-padded to length ``n_fft``.
    n_fft : int
        Number of FFT points. Much faster for powers of two.
    step : int | None
        Step size between calculations. If None, ``n_fft // 2``
        will be used.
    fs : float
        The sample rate of the data.
    window : str | None
        Window function to use. Can be ``'hann'`` for Hann window, or None
        for no windowing.
    normalize : bool
        Normalization of spectrogram values across frequencies.
    color_scale : {'linear', 'log'}
        Scale to apply to the result of the STFT.
        ``'log'`` will use ``10 * log10(power)``.
    cmap : str
        Colormap name.
    clim : str | tuple
        Colormap limits. Should be ``'auto'`` or a two-element tuple of
        min and max values.
    """

    def __init__(self, x=None, n_fft=256, step=None, fs=1., window='hann',
                 normalize=False, color_scale='log', cmap='cubehelix',
                 clim='auto'):
        self._x = np.asarray(x)
        self._n_fft = int(n_fft)
        self._step = step
        self._fs = float(fs)
        self._window = window
        self._normalize = normalize
        self._color_scale = color_scale

        if clim == 'auto':
            self._clim_auto = True
        else:
            self._clim_auto = False

        if not isinstance(self._color_scale, str) or \
                self._color_scale not in ('log', 'linear'):
            raise ValueError('color_scale must be "linear" or "log"')

        data = self._calculate_spectrogram()
        super(SpectrogramVisual, self).__init__(data, clim=clim, cmap=cmap)

    @property
    def freqs(self):
        """The spectrogram frequencies"""
        return fft_freqs(self._n_fft, self._fs)

    @property
    def x(self):
        """The original signal"""
        return self._x

    @x.setter
    def x(self, x):
        self._x = np.asarray(x)
        self._update_image()

    @property
    def n_fft(self):
        """The length of fft window"""
        return self._n_fft

    @n_fft.setter
    def n_fft(self, n_fft):
        self._n_fft = int(n_fft)
        self._update_image()

    @property
    def step(self):
        """The step of fft window"""
        if self._step is None:
            return self._n_fft // 2
        else:
            return self._step

    @step.setter
    def step(self, step):
        self._step = step
        self._update_image()

    @property
    def fs(self):
        """The sampling frequency"""
        return self._fs

    @fs.setter
    def fs(self, fs):
        self._fs = fs
        self._update_image()

    @property
    def window(self):
        """The used window function"""
        return self._window

    @window.setter
    def window(self, window):
        self._window = window
        self._update_image()

    @property
    def color_scale(self):
        """The color scale"""
        return self._color_scale

    @color_scale.setter
    def color_scale(self, color_scale):
        if not isinstance(color_scale, str) or \
                color_scale not in ('log', 'linear'):
            raise ValueError('color_scale must be "linear" or "log"')
        self._color_scale = color_scale
        self._update_image()

    @property
    def normalize(self):
        """The normalization setting"""
        return self._normalize

    @normalize.setter
    def normalize(self, normalize):
        self._normalize = normalize
        self._update_image()

    def _calculate_spectrogram(self):
        if self._x is not None:
            x = self._x
            nan_mean = np.nanmean(x)
            idx = np.isnan(x)
            x[idx] = nan_mean
            data = stft(x, self._n_fft, self._step, self._fs, self._window)
            data = np.abs(data)
            data = 20 * np.log10(data) if self._color_scale == 'log' else data
            if self._normalize:
                for i in range(data.shape[0]):
                    data[i, :] -= np.mean(data[i, :])
                    data[i, :] /= np.std(data[i, :])
            return data.astype(np.float32)  # ImageVisual will warn if 64-bit
        else:
            return None

    def _update_image(self):
        data = self._calculate_spectrogram()
        self.set_data(data)
        self.update()
        if self._clim_auto:
            self.clim = 'auto'