import numpy as np
import sounddevice as sd
class play:
    def __init__(self, fs=44100):
        self.fs, self.active = fs, []
        self.stream = sd.OutputStream(samplerate=fs, channels=1, callback=self._cb)
        self.stream.start()
    def _cb(self, out, frames, time, status):
        out.fill(0)
        for b in self.active[:]:
            start_f = b['f']
            end_f = start_f + frames
            t = np.arange(start_f, end_f) / self.fs
            wave = np.sin(2 * np.pi * b['hz'] * t)
            full_t = np.arange(start_f, end_f) / b['total']
            envelope = np.ones(frames)
            fade_in_len = int(b['total'] * 0.1)
            if start_f < fade_in_len:
                local_fade = np.linspace(0, 1, fade_in_len)
                needed = min(frames, fade_in_len - start_f)
                envelope[:needed] = local_fade[start_f: start_f + needed]
            fade_out_start = b['total'] - fade_in_len
            if end_f > fade_out_start:
                local_fade = np.linspace(1, 0, fade_in_len)
                offset = max(0, fade_out_start - start_f)
                remaining = frames - offset
                if remaining > 0:
                    envelope[offset:] = local_fade[:remaining]
            out[:, 0] += (wave * envelope * 0.02).astype(np.float32)
            b['f'] += frames
            if b['f'] >= b['total']: self.active.remove(b)
    def beep(self, hz, duration):
        if hz > 20:
            self.active.append({'hz': hz, 'total': int(duration * self.fs), 'f': 0})