class Tone:
- # A wave, at a frequency and amplitude
- def __init__(self, wave, frequency, amplitude, adsr=None):
+ # A wave, at a frequency and amplitude started after counter samples.
+ def __init__(self, wave, frequency, amplitude, counter, adsr=None):
self.wave = wave
assert not isinstance(frequency, str)
self.frequency = frequency
self.amplitude = amplitude
+ self.counter = counter
self.adsr = adsr
self.signal = True
def __call__(self, n):
- value = self.wave(self.frequency * n) * self.amplitude
+ value = self.wave(self.frequency * (n - self.counter)) * self.amplitude
if self.adsr is not None:
value *= self.adsr(self.signal)
return value
def release(self):
self.signal = False
- def push(self):
+ def push(self, counter):
self.signal = True
+ self.counter = counter
class ADSR:
from visualization import KeyVisualization
FPS = 60
-CHUNKS_PER_SECOND = 20
+CHUNKS_PER_SECOND = 5
+MASTER_VOLUME = .05 # expect noise when adding >= 20 voices
class ChannelManager:
basic_sample_type = {-8: numpy.int8, -16: numpy.int16}[sample_format]
self.sample_type = numpy.dtype((basic_sample_type, 2))
self.wave = SineWave(self.sample_rate)
- self.amplitude = 2 ** (abs(sample_format) - 1) - 1
+ self.amplitude = int(
+ (2 ** (abs(sample_format) - 1) - 1) * MASTER_VOLUME
+ )
self.envelope = ADSR.Envelope(s(.2), s(.4), .75, s(.5))
self.tones = {}
self.counter = 0
- self.eta = time() + 1 / chunks_per_second
+ self.eta = None
@staticmethod
def duplicate_channel(g):
return ((e, e) for e in g)
- @staticmethod
- def queue(channel, *args):
- channel.queue(
- pygame.sndarray.make_sound(
- numpy.fromiter(*args)
- )
- )
-
def update(self):
+ t = time()
for frequency in {
frequency for frequency, tone in self.tones.items()
if not tone.has_signal()
if self.channel:
self.channel = None
self.counter = 0
- self.eta = 0
+ self.eta = None
return
if self.channel is None:
self.channel = pygame.mixer.Channel(True)
if self.channel.get_busy() and self.channel.get_queue() is not None:
return
- t = time()
- print("gening", t, self.eta)
- if self.eta is not None and self.eta < t:
- print("buffer underrun")
- sample_slice = slice(self.counter, self.counter + self.chunk_size)
- self.queue(
- self.channel,
- self.duplicate_channel(
- int(
- sum(tone(n) for tone in self.tones.values())
- / len(self.tones)
- )
- for n in range(sample_slice.start, sample_slice.stop)
- ),
- self.sample_type
+ stop_counter = self.counter + self.chunk_size
+ snd = pygame.sndarray.make_sound(
+ numpy.fromiter(
+ self.duplicate_channel(
+ int(sum(tone(n) for tone in self.tones.values()))
+ for n in range(self.counter, stop_counter)
+ ),
+ self.sample_type
+ )
)
- self.counter = sample_slice.stop
+ if self.eta is not None and self.eta < t:
+ msg_prefix = "likely " if self.channel.get_busy() else ""
+ print(f"{msg_prefix}buffer underrun", self.eta - t)
self.eta = time() + self.chunk_size / self.sample_rate
+ if not self.channel.get_busy():
+ self.channel.play(snd)
+ else:
+ self.channel.queue(snd)
+ self.counter = stop_counter
def update_tones(self, frequencies):
- have_keys = {frequency for frequency, tone in self.tones.items() if tone.signal}
+ have_keys = {
+ frequency for frequency, tone in self.tones.items() if tone.signal
+ }
if frequencies == have_keys:
return
for frequency in frequencies - have_keys:
if frequency in self.tones:
- self.tones[frequency].push()
+ self.tones[frequency].push(self.counter)
continue
self.tones[frequency] = Tone(
self.wave,
frequency,
self.amplitude,
+ self.counter,
ADSR(self.envelope),
)
for frequency in have_keys - frequencies:
class MusicBox:
def __init__(self):
- pygame.mixer.pre_init(buffer=2048)
+ pygame.mixer.pre_init()
pygame.init()
assert hasattr(pygame.constants, "FINGERMOTION")
self.surf = pygame.display.set_mode(flags=pygame.FULLSCREEN)