• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

int-brain-lab / iblrig / 9032957364

10 May 2024 01:25PM UTC coverage: 48.538% (+1.7%) from 46.79%
9032957364

Pull #643

github

74d2ec
web-flow
Merge aebf2c9af into ec2d8e4fe
Pull Request #643: 8.19.0

377 of 1074 new or added lines in 38 files covered. (35.1%)

977 existing lines in 19 files now uncovered.

3253 of 6702 relevant lines covered (48.54%)

0.97 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

36.0
/iblrig/sound.py
1
import logging
2✔
2

3
import numpy as np
2✔
4
from scipy.signal import chirp
2✔
5

6
from pybpod_soundcard_module.module_api import DataType, SampleRate, SoundCardModule
2✔
7

8
log = logging.getLogger(__name__)
2✔
9

10

11
def make_sound(rate=44100, frequency=5000, duration=0.1, amplitude=1, fade=0.01, chans='L+TTL'):
2✔
12
    """
13
    Build sounds and save bin file for upload to soundcard or play via
14
    sounddevice lib.
15

16
    :param rate: sample rate of the soundcard use 96000 for Bpod,
17
                    defaults to 44100 for soundcard
18
    :type rate: int, optional
19
    :param frequency: (Hz) of the tone, if -1 will create uniform random white
20
                    noise, defaults to 10000
21
    :type frequency: int, optional
22
    :param duration: (s) of sound, defaults to 0.1
23
    :type duration: float, optional
24
    :param amplitude: E[0, 1] of the sound 1=max 0=min, defaults to 1
25
    :type amplitude: intor float, optional
26
    :param fade: (s) time of fading window rise and decay, defaults to 0.01
27
    :type fade: float, optional
28
    :param chans: ['mono', 'L', 'R', 'stereo', 'L+TTL', 'TTL+R'] number of
29
                   sound channels and type of output, defaults to 'L+TTL'
30
    :type chans: str, optional
31
    :return: streo sound from mono definitions
32
    :rtype: np.ndarray with shape (Nsamples, 2)
33
    """
34
    sample_rate = rate  # Sound card dependent,
2✔
35
    tone_duration = duration  # sec
2✔
36
    fade_duration = fade  # sec
2✔
37
    chans = chans if isinstance(chans, str) else chans[0]
2✔
38
    tvec = np.linspace(0, tone_duration, int(tone_duration * sample_rate))
2✔
39
    tone = amplitude * np.sin(2 * np.pi * frequency * tvec)  # tone vec
2✔
40

41
    len_fade = int(fade_duration * sample_rate)
2✔
42
    fade_io = np.hanning(len_fade * 2)
2✔
43
    fadein = fade_io[:len_fade]
2✔
44
    fadeout = fade_io[len_fade:]
2✔
45
    win = np.ones(len(tvec))
2✔
46
    win[:len_fade] = fadein
2✔
47
    win[-len_fade:] = fadeout
2✔
48

49
    tone = tone * win
2✔
50
    ttl = np.ones(len(tone)) * 0.99
2✔
51
    one_ms = round(sample_rate / 1000) * 10
2✔
52
    ttl[one_ms:] = 0
2✔
53
    null = np.zeros(len(tone))
2✔
54

55
    if frequency == -1:
2✔
56
        tone = amplitude * np.random.rand(tone.size)
2✔
57

58
    if chans == 'mono':
2✔
UNCOV
59
        sound = np.array(tone)
×
60
    elif chans == 'L':
2✔
UNCOV
61
        sound = np.array([tone, null]).T
×
62
    elif chans == 'R':
2✔
UNCOV
63
        sound = np.array([null, tone]).T
×
64
    elif chans == 'stereo':
2✔
65
        sound = np.array([tone, tone]).T
2✔
66
    elif chans == 'L+TTL':
×
67
        sound = np.array([tone, ttl]).T
×
68
    elif chans == 'TTL+R':
×
UNCOV
69
        sound = np.array([ttl, tone]).T
×
70

71
    return sound
2✔
72

73

74
def make_chirp(f0=80, f1=160, length=0.1, amp=0.1, fade=0.01, sf=96000):
2✔
75
    t0 = 0
×
76
    t1 = length
×
UNCOV
77
    t = np.linspace(t0, t1, sf)
×
78

UNCOV
79
    c = amp * chirp(t, f0=f0, f1=f1, t1=t1, method='linear')
×
80

81
    len_fade = int(fade * sf)
×
82
    fade_io = np.hanning(len_fade * 2)
×
83
    fadein = fade_io[:len_fade]
×
84
    fadeout = fade_io[len_fade:]
×
85
    win = np.ones(len(t))
×
86
    win[:len_fade] = fadein
×
UNCOV
87
    win[-len_fade:] = fadeout
×
88

89
    c = c * win
×
90
    out = np.array([c, c]).T
×
UNCOV
91
    return out
×
92

93

94
def format_sound(sound, file_path=None, flat=False):
2✔
95
    """
96
    Format sound to send to sound card.
97

98
    Binary files to be sent to the sound card need to be a single contiguous
99
    vector of int32 s. 4 Bytes left speaker, 4 Bytes right speaker, ..., etc.
100

101

102
    :param sound: Stereo sound
103
    :type sound: 2d numpy.array os shape (n_samples, 2)
104
    :param file_path: full path of file. [default: None]
105
    :type file_path: str
106
    """
UNCOV
107
    bin_sound = (sound * ((2**31) - 1)).astype(np.int32)
×
108

109
    if bin_sound.flags.f_contiguous:
×
UNCOV
110
        bin_sound = np.ascontiguousarray(bin_sound)
×
111

112
    bin_save = bin_sound.reshape(1, np.multiply(*bin_sound.shape))
×
UNCOV
113
    bin_save = np.ascontiguousarray(bin_save)
×
114

115
    if file_path:
×
116
        with open(file_path, 'wb') as bf:
×
117
            bf.writelines(bin_save)
×
UNCOV
118
            bf.flush()
×
119

UNCOV
120
    return bin_sound.flatten() if flat else bin_sound
×
121

122

123
def configure_sound_card(card=None, sounds=None, indexes=None, sample_rate=96):
2✔
124
    if indexes is None:
×
125
        indexes = []
×
UNCOV
126
    if sounds is None:
×
127
        sounds = []
×
128
    if card is None:
×
129
        card = SoundCardModule()
×
130
        close_card = True
×
131

132
    if sample_rate in (192, 192000):
×
133
        sample_rate = SampleRate._192000HZ
×
UNCOV
134
    elif sample_rate in (96, 96000):
×
135
        sample_rate = SampleRate._96000HZ
×
136
    else:
137
        log.error(f'Sound sample rate {sample_rate} should be 96 or 192 (KHz)')
×
UNCOV
138
        raise (ValueError)
×
139

140
    if len(sounds) != len(indexes):
×
141
        log.error('Wrong number of sounds and indexes')
×
UNCOV
142
        raise (ValueError)
×
143

144
    sounds = [format_sound(s, flat=True) for s in sounds]
×
NEW
145
    for sound, index in zip(sounds, indexes, strict=False):
×
UNCOV
146
        card.send_sound(sound, index, sample_rate, DataType.INT32)
×
147

UNCOV
148
    if close_card:
×
UNCOV
149
        card.close()
×
150

151

152
# FIXME: in _passiveCW use SoundCardModule to give to this v instead of finding device yourself
153
def trigger_sc_sound(sound_idx, card=None):
2✔
UNCOV
154
    if card is None:
×
UNCOV
155
        card = SoundCardModule()
×
UNCOV
156
        close_card = True
×
157
    # [MessageType] [Length] [Address] [Port] [PayloadType] [Payload] [Checksum]
158
    # write=2 LEN=6 addr=32 port=255 payloadType=2 payload=[index 0]U16 checksum=43
159

160
    # 2 6 32 255 2 [2 0] 43 --> play tone
161
    # 2 6 32 255 2 [3 0] 44 --> play noise
162

163
    # 2 LEN=5 33 255 1 [index]U8 checksum
164

165
    def _calc_checksum(data):
×
166
        return sum(data) & 0xFF
×
167

UNCOV
168
    sound_idx = int(sound_idx)
×
169
    message = [2, 6, 32, 255, 2, sound_idx, 0]
×
UNCOV
170
    message.append(_calc_checksum(message))
×
171
    message = bytes(np.array(message, dtype=np.int8))
×
172
    # usb.write(port, message, timeout)
UNCOV
173
    card._dev.write(1, message, 200)
×
174

UNCOV
175
    if close_card:
×
UNCOV
176
        card.close()
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc