減衰音を付けてみた

ポルタメントしてみる - つちのこ、のこのこ。(はてな番外地)」に半音合わせで発音する減衰音加えてみました。マウス左ボタンを離した時にいちばん近い半音に合わせて発音です。(ガイドラインはピアノの白鍵の位置のみに引かれていますが発音位置はそれに限られません)

マウスの中ボタンまたはスペースバーで、その減衰音のみを停止します。
(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()
# 好きに流用してください。

これでなんとか演奏できるようには。

しかしだんだんとソースがどろどろに。そのうちにちゃんと書き直さないと。