減衰音を付けてみた
「ポルタメントしてみる - つちのこ、のこのこ。(はてな番外地)」に半音合わせで発音する減衰音加えてみました。マウス左ボタンを離した時にいちばん近い半音に合わせて発音です。(ガイドラインはピアノの白鍵の位置のみに引かれていますが発音位置はそれに限られません)
マウスの中ボタンまたはスペースバーで、その減衰音のみを停止します。
(Python 2.6 + Pygame 1.8)
from __future__ import division import wave import math import array import pygame from pygame.locals import * FULLSCREEN = False # True にするとフルスクリーン表示 FPS = 60 # 秒間描画枚数 WIDTH, HEIGHT = 640, 480 # 表示する画面のサイズ LINEWIDTH = 20 # 線の太さ COLOR = 0, 255, 200 # 色 BG_COLOR = 0, 0, 50 # 背景色 BASE_FREQUENCY = 220 # 左端での周波数(Hz) OCTAVE_WIDTH = 240 # 1オクターブの幅 FADEOUT_TIME = 5000 # 減衰音の時間(ミリ秒) # 作業用 wave ファイルのスペック(実際の発音は pygame の設定に依存) FILE_FREQUENCY = 44100 WAVE_LENGTH = 1/8 DUMMY_WAVE = '__dummy__.wav' class Tone(object): def __init__(self, frequency): ## print frequency self.base_frequency = frequency self.wave_length = WAVE_LENGTH * 8 filename = DUMMY_WAVE f = wave.open(filename, 'wb') f.setparams((1, 2, FILE_FREQUENCY, 0, 'NONE', 'not compressed')) f.writeframes( array.array('h', [0] * int( FILE_FREQUENCY * self.wave_length)).tostring()) f.close() self.sound = pygame.mixer.Sound(filename) self.octave = None def play(self, octave, fadeout=FADEOUT_TIME): if self.octave is None or self.octave != octave: self.octave = octave frequency = self.base_frequency * (2 ** octave) length = int(PYGAME_FREQUENCY * self.wave_length) data = array.array('h', [0] * 2 * length) a = math.pi * 2 * frequency / PYGAME_FREQUENCY vol = min(32767, 32767 * BASE_FREQUENCY / frequency) for i in range(length): data[i * 2] = data[i * 2 + 1] = int(math.sin(a * i) * vol) self.sound.get_buffer().write(data, 0) self.sound.stop() self.sound.play(-1) self.sound.fadeout(fadeout) def stop(self): self.sound.stop() class Tones(object): def __init__(self, base_frequency=BASE_FREQUENCY): self.tone = [] log_base = math.log(base_frequency, 2) for i in range(12): frequency = 2 ** (log_base + i / 12) self.tone.append(Tone(frequency)) def play(self, octave, semitone): self.tone[int(semitone)].play(octave) def stop(self): for i in self.tone: i.stop() def dummy_wave(length=WAVE_LENGTH): f = wave.open(DUMMY_WAVE, 'wb') f.setparams((1, 2, FILE_FREQUENCY, 0, 'NONE', 'not compressed')) f.writeframes( array.array('h', [0] * int(FILE_FREQUENCY * length)).tostring()) f.close() def write_sin(buf, freqency, volume=1, length=WAVE_LENGTH): data = array.array('h', [0] * 2 * int(PYGAME_FREQUENCY * length)) a = math.pi * 2 * freqency / PYGAME_FREQUENCY vol = min(32767, 32767 * volume) for i in range(int(PYGAME_FREQUENCY * length)): data[i * 2] = data[i * 2 + 1] = int(math.sin(a * i) * vol) buf.write(data, 0) def set_wave(sound, frequency): write_sin(sound.get_buffer(), frequency, BASE_FREQUENCY / frequency) def main(): dummy_wave() sound = pygame.mixer.Sound(DUMMY_WAVE) screen = pygame.display.set_mode( (WIDTH, HEIGHT), (pygame.FULLSCREEN | pygame.HWSURFACE | pygame.DOUBLEBUF) if FULLSCREEN else 0) timer = pygame.time.Clock() sound_on = False log_base = math.log(BASE_FREQUENCY, 2) old_x = -1 tones = Tones() while True: timer.tick(FPS) screen.fill(BG_COLOR) x, y = pygame.mouse.get_pos() button1, button2, button3 = pygame.mouse.get_pressed() focuse = pygame.mouse.get_focused() a = (x / OCTAVE_WIDTH) frequency = 2 ** (log_base + a) if focuse: if button1: pygame.draw.line( screen, COLOR, (x, 0), (x, HEIGHT - 1), LINEWIDTH) if sound_on: if old_x != x: set_wave(sound, frequency) old_x = x else: set_wave(sound, frequency) old_x = x sound.play(-1) sound_on = True else: pygame.draw.line( screen, (255, 255, 0), (x, 0), (x, HEIGHT - 1), 3) sound.stop() sound_on = False old_x = -1 else: sound.stop() sound_on = False old_x = -1 for xb in range(0, WIDTH, OCTAVE_WIDTH): for s, c in ((0, (69, 0, 184)), (2, (23, 0, 230)), (3, (253, 0, 0)), (5, (207, 0, 46)), (7, (184, 0, 69)), (8, (138, 0, 115)), (10, (92, 0, 161))): x = xb + s * OCTAVE_WIDTH / 12 pygame.draw.line(screen, c, (x, 0), (x, HEIGHT - 1), 1) pygame.display.flip() cap = '%5.2f fps, %7.2fHz' % ( timer.get_fps(), frequency) pygame.display.set_caption(cap) for event in pygame.event.get(): if event.type == QUIT: return elif event.type == KEYDOWN and event.key == K_ESCAPE: return elif event.type == MOUSEBUTTONUP and event.button == 1: x, y = event.pos note = int(round(x * 12 / OCTAVE_WIDTH)) tones.play(note // 12, note % 12) elif event.type == MOUSEBUTTONDOWN and event.button == 2: tones.stop() elif event.type == KEYDOWN and event.unicode == u' ': tones.stop() if __name__=='__main__': pygame.mixer.pre_init(44100, -16, 2) pygame.init() PYGAME_FREQUENCY, format, PYGAME_CHANNELS = pygame.mixer.get_init() try: main() finally: pygame.quit() # 好きに流用してください。
これでなんとか演奏できるようには。
しかしだんだんとソースがどろどろに。そのうちにちゃんと書き直さないと。