珈琲焙煎者の観察

地下部屋の珈琲焙煎者

インベーダーゲームのつづき

インベーダーゲームの作り方。

manuke.hateblo.jp

上記記事からのつづきになっています。

よりインベーダーゲームぽく。

import

from pygame import *
import sys
from os.path import abspath, dirname
from random import choice
from operator import itemgetter, attrgetter

path

# ファイルへの path
BASE_PATH = abspath(dirname(__file__))
FONT_PATH = BASE_PATH + '/fonts/'
IMAGE_PATH = BASE_PATH + '/images/'
SOUND_PATH =  BASE_PATH + '/sounds/'

RGB

# RGB値を変数 WHITE,GREEN,YELLOW,BLUE,PURPLE,RED へ用意する
WHITE = (255, 255, 255)
GREEN = (78, 255, 87)
YELLOW = (241, 255, 0)
BLUE = (80, 255, 239)
PURPLE = (203, 0, 255)
RED = (237, 28, 36)

screen , font , images

# スクリーンのサイズをpygame.Surfaceオブジェクトとして渡す
SCREEN = display.set_mode((800, 600),SCALED)
FONT = FONT_PATH + 'space_invaders.ttf'
IMG_NAMES = ['ship','shipexplosion1','shipexplosion2','shipexplosion3','mystery',
            'enemy1_1','enemy1_2',                                            'enemy2_1','enemy2_2',
            'enemy3_1','enemy3_2',
            'explosionblue','explosiongreen','explosionpurple',
            'laser','laser1','enemylaser','enemylaser1']
IMAGES = {name: image.load(IMAGE_PATH + '{}.png'.format(name)).convert_alpha()
        for name in IMG_NAMES}

# ポジション初期値
BLOCKERS_POSITION = 450 # トーチカ
ENEMY_DEFAULT_POSITION = 65 # ゲームスタートの時のエイリアンの高さ
ENEMY_MOVE_DOWN = 35 # エイリアンの近づいてくる

ROUND_NUM = 0
SHIP_VX = 10
ROW_counter = 4
LEVEL = 700
# クラス定義
class Ship(sprite.Sprite): # player のアイコン
    def __init__(self):
        sprite.Sprite.__init__(self)
        self.image = IMAGES['ship']
        self.image = transform.scale(self.image,(50,50))
        self.rect = self.image.get_rect(topleft=(375, 540))
        self.dist_x = self.rect.x
        self.speed = 1
        self.vx = SHIP_VX

    def update(self, keys, *args): # 画面左右 player が動ける範囲
        if keys[K_a] and self.rect.x > 10:
            # self.rect.x -= self.speed # 左移動
            self.vx += self.speed
            self.dist_x = (self.rect.x - self.vx)
            self.rect.x += (-(-(self.dist_x - self.rect.x))//3)                                                                                                                                                   if self.rect.x < 10: # 画面左端まできたら止まる
                self.rect.x = 10
            # キーアップで速度vxをSHIP_VXに戻す check_input()
        if keys[K_d] and self.rect.x < 740:
            # self.rect.x += self.speed # 右移動
            self.vx += self.speed                                                                                                                                                                                 self.dist_x = (self.rect.x + self.vx)
            self.rect.x += (-(-(self.dist_x - self.rect.x))//3)
            if self.rect.x > 740: # 画面右端まできたら止まる
                self.rect.x = 740
            # キーアップで速度vxをSHIP_VXに戻す check_input()
        game.screen.blit(self.image, self.rect) # Surfaceオブジェクトにblit

Bullet

class Bullet(sprite.Sprite): # 弾
    def __init__(self, xpos, ypos, direction, speed, filename, side):
        sprite.Sprite.__init__(self)
        self.image = IMAGES[filename]                                                                      self.image2 = IMAGES[filename + '1']
        self.rect = self.image.get_rect(topleft=(xpos, ypos))
        self.speed = speed
        self.direction = direction
        self.side = side
        self.filename = filename
        self.timer = time.get_ticks()                                                              
    def update(self, keys, *args):
        game.screen.blit(self.image, self.rect)
        self.rect.y += self.speed * self.direction
        current_time = time.get_ticks()
                                                                                                                                                                                                              if self.rect.y < 35:                                                                                   self.rect.y = 35
            game.screen.blit(self.image2, self.rect)
            if current_time > self.timer + 700:
                self.kill() # 画面の上部で弾の表示を消す

        if self.rect.y > 585:
            self.rect.y = 580
            game.screen.blit(self.image2, self.rect)
            if current_time > self.timer + 160:
                self.kill() # 画面の下部で弾の表示を消
# 弾の変化
class bulletExplosion(sprite.Sprite):
    def __init__(self, enemylaser, *groups):
        super(bulletExplosion, self).__init__(*groups)
        self.image = IMAGES['enemylaser1']
        self.rect = self.image.get_rect(topleft=(enemylaser.rect.x, enemylaser.rect.y))                    self.timer = time.get_ticks()

    def update(self, current_time, *args):
        passed = current_time - self.timer
        if passed <= 300:
           game.screen.blit(self.image, self.rect)
        elif 300 < passed:
           self.kill()

Enemy

class Enemy(sprite.Sprite): # 敵キャラクター
    def __init__(self, row, column):
        sprite.Sprite.__init__(self)
        self.row = row
        self.column = column
        self.images = []
        self.load_images()
        self.index = 0
        self.image = self.images[self.index]
        self.rect = self.image.get_rect()
        self.moveTime = 200
        self.timeOffset = row * (-50) + column * 120
        self.timer = time.get_ticks() + self.timeOffset
        global LEVEL
        LEVEL = 700
    #アニメーション
    def toggle_image(self):
        self.index += 1
        if self.index >= len(self.images):
            self.index = 0
        self.image = self.images[self.index]

    def update(self, *args):
        current_time = time.get_ticks()
        if current_time - self.timer > self.moveTime:
            game.screen.blit(self.image, self.rect)
        #self.timer += self.moveTime
        
    def load_images(self):
        images = {0:['1_2', '1_1'],1:['2_2', '2_1'],2:['2_2', '2_1'],3:['3_1', '3_2'],4:['3_1', '3_2']}
        img1, img2 = (IMAGES['enemy{}'.format(img_num)] for img_num in images[self.row])
        self.images.append(transform.scale(img1, (40, 35)))
        self.images.append(transform.scale(img2, (40, 35)))

EnemiesGroup

class EnemiesGroup(sprite.Group):
    def __init__(self, columns, rows):
        sprite.Group.__init__(self)
        self.enemies = [[None] * columns for _ in range(rows)]
        self.columns = columns
        self.rows = rows
        self.leftAddMove = 0
        self.rightAddMove = 0
        self.moveTime = 200
        self.direction = 1
        self.rightMoves = 30
        self.leftMoves = 30
        self.moveNumber = 15
        # ラウンドが上がると敵が下へ降りてくる
        self.timeOffset = 22000 * ROUND_NUM
        self.timer = time.get_ticks() - self.timeOffset
        self.speedup = 0
        #self.timer = time.get_ticks()
        self.bottom = game.enemyPosition + ((rows - 1) * 45) +35
        self._aliveColumns = list(range(columns))
        self._leftAliveColumn = 0
        self._rightAliveColumn = columns - 1
        self.minRow = 0
        self.maxRow = 4

    def update(self, current_time):
        if current_time - self.timer > self.moveTime:
            if self.direction == 1:
                max_move = self.rightMoves + self.rightAddMove

            else:
                max_move = self.leftMoves + self.leftAddMove

            if self.moveNumber >= max_move:
                self.leftMoves = 30 + self.rightAddMove
                self.rightMoves = 30 + self.leftAddMove
                self.direction *= -1
                self.moveNumber = 0
                self.bottom = 0
                for enemy in self:
                    enemy.rect.y += ENEMY_MOVE_DOWN
                    enemy.toggle_image()
                    if self.bottom < enemy.rect.y + 35:
                        self.bottom = enemy.rect.y + 35
            else:
                velocity = 10 if self.direction == 1 else -10
                global ROW_counter
                
                s = sorted(self, key=attrgetter('row'),reverse=True)
                for enemy in s:
                #for enemy in self:
                    
                    if len(self) <= 4: # skip check when 4 enemies
                        enemy.rect.x += velocity
                        enemy.toggle_image()
                        continue # goto loop start
                    
                    row_i = enemy.row
                    
                    if row_i == ROW_counter:
                        enemy.rect.x += velocity
                        enemy.toggle_image()
                        
                    else:
                        continue
                
                if len(self) <= 4:
                    self.moveNumber += 1    
                else:
                    ROW_counter -= 1
                    if ROW_counter == self.minRow -1:
                        ROW_counter = self.maxRow
                        self.moveNumber += 1

            self.timer += self.moveTime

    def add_internal(self, *sprites):
        super(EnemiesGroup, self).add_internal(*sprites)
        for s in sprites:
            self.enemies[s.row][s.column] = s

    def remove_internal(self, *sprites):
        super(EnemiesGroup, self).remove_internal(*sprites)
        for s in sprites:
            self.kill(s)
        self.update_speed()

    def is_column_dead(self, column):
        return not any(self.enemies[row][column]
                        for row in range(self.rows))

    def random_bottom(self):
        col = choice(self._aliveColumns)
        col_enemies = (self.enemies[row - 1][col]
                        for row in range(self.rows, 0, -1))
        return next((en for en in col_enemies if en is not None), None)

    def update_speed(self):
        # 敵の数が減れば段階的にスピードアップする
        global LEVEL
        minimum = 5
        maxmum = 0
        for l in self:
            compRow = l.row    
            if compRow < minimum:
                minimum = compRow            
            if compRow > maxmum:
                maxmum = compRow
            self.minRow = minimum
            self.maxRow = maxmum

        if len(self) == 1: # 敵の数が1になれば、敵の動きがすごく速くなる
            self.moveTime = 2
            LEVEL = 350
#=============================================================================
        elif len(self) ==  2:
            self.moveTime = 40
        elif len(self) ==  3:
            self.moveTime = 60
        elif len(self) ==  4:
             self.moveTime = 60
        elif len(self) <=  10:
             self.moveTime = 40
        elif len(self) <=  15:
             self.moveTime = 80
        elif len(self) <= 20:
             self.moveTime = 90
        elif len(self) <= 30: # 敵の数が30になれば、敵の動きが速くなる↑
             self.moveTime = 100
        elif len(self) <= 40: # 敵の数が40になれば、敵の動きが速くなる↑
             self.moveTime = 110
             LEVEL = 400
#=============================================================================

    def kill(self, enemy):
        self.enemies[enemy.row][enemy.column] = None
        is_column_dead = self.is_column_dead(enemy.column)

        if is_column_dead:
            self._aliveColumns.remove(enemy.column)

        if enemy.column == self._rightAliveColumn:
            while self._rightAliveColumn > 0 and is_column_dead:
                self._rightAliveColumn -= 1
                self.rightAddMove += 5
                is_column_dead = self.is_column_dead(self._rightAliveColumn)

        elif enemy.column == self._leftAliveColumn:
            while self._leftAliveColumn < self.columns and is_column_dead:
                self._leftAliveColumn += 1
                self.leftAddMove += 5
                is_column_dead = self.is_column_dead(self._leftAliveColumn)

Blocker

class Blocker(sprite.Sprite):
    def __init__(self, size, color, row, column):
        sprite.Sprite.__init__(self)
        self.height = size
        self.width = size
        self.color = color
        self.image = Surface((self.width, self.height))
        self.image.fill(self.color)
        self.rect = self.image.get_rect()
        self.row = row
        self.column = column

    def update(self, keys, *args):
        game.screen.blit(self.image, self.rect)

Mystery

class Mystery(sprite.Sprite):
    def __init__(self):
        sprite.Sprite.__init__(self)
        self.image = IMAGES['mystery']
        self.image = transform.scale(self.image, (80, 40))
        self.rect = self.image.get_rect(topleft=(-80, 25))
        self.row = 5
        self.moveTime = 25000
        self.direction = 1
        self.timer = time.get_ticks()
        self.mysteryEntered = mixer.Sound(SOUND_PATH + 'mysteryentered.wav')
        self.mysteryEntered.set_volume(0.1)
        self.playSound = True

    def update(self, keys, currentTime, *args):
        resetTimer = False
        passed = currentTime - self.timer
        if passed > self.moveTime:
            if (self.rect.x < 0 or self.rect.x > 800) and self.playSound:
                self.mysteryEntered.play()
                self.playSound = False
            if self.rect.x < 840 and self.direction == 1:
                self.mysteryEntered.fadeout(4000)
                self.rect.x += 2
                game.screen.blit(self.image, self.rect)
            if self.rect.x > -100 and self.direction == -1:
                self.mysteryEntered.fadeout(4000)
                self.rect.x -= 2
                game.screen.blit(self.image, self.rect)

        if self.rect.x > 830:
            self.playSound = True
            self.direction = -1
            resetTimer = True
        if self.rect.x < -90:
            self.playSound = True
            self.direction = 1
            resetTimer = True
        if passed > self.moveTime and resetTimer:
            self.timer = currentTime

EnemyExplosion

class EnemyExplosion(sprite.Sprite):
    def __init__(self, enemy, *groups):
        super(EnemyExplosion, self).__init__(*groups)
        self.image = transform.scale(self.get_image(enemy.row), (40, 35))
        self.image2 = transform.scale(self.get_image(enemy.row), (50, 45))
        self.rect = self.image.get_rect(topleft=(enemy.rect.x, enemy.rect.y))
        self.timer = time.get_ticks()

    @staticmethod
    def get_image(row):
        img_colors = ['purple', 'blue', 'blue', 'green', 'green']
        return IMAGES['explosion{}'.format(img_colors[row])]

    def update(self, current_time, *args):
        passed = current_time - self.timer
        if passed <= 100:
           game.screen.blit(self.image, self.rect)
        elif passed <= 200:
           game.screen.blit(self.image2, (self.rect.x -6, self.rect.y -6))
        elif 400 < passed:
           self.kill()
class MysteryExplosion(sprite.Sprite):
    def __init__(self, mystery, score, *groups):
        super(MysteryExplosion, self).__init__(*groups)
        self.text = Text(FONT, 20, str(score), WHITE, mystery.rect.x + 20, mystery.rect.y + 6)
        self.timer = time.get_ticks()

    def update(self, current_time, *args):
        passed = current_time - self.timer
        if passed <= 200 or 400 < passed <= 600:
            self.text.draw(game.screen)
        elif 600 < passed:
            self.kill()
class ShipExplosion(sprite.Sprite):
    def __init__(self, ship, *groups):
        super(ShipExplosion, self).__init__(*groups)
        self.image = IMAGES['shipexplosion1']
        self.image2 = IMAGES['shipexplosion2']
        self.image3 = IMAGES['shipexplosion3']
        self.rect = self.image.get_rect(topleft=(ship.rect.x, ship.rect.y))
        self.timer = time.get_ticks()

    def update(self, current_time, *args):
        passed = current_time - self.timer
        if passed <= 100:
            game.screen.blit(self.image, self.rect)
        elif 100 < passed <= 200:
            game.screen.blit(self.image2, self.rect)
        elif 200< passed <= 300:
            game.screen.blit(self.image3, self.rect)
        elif 300 < passed <= 400:
            game.screen.blit(self.image2, self.rect)
        elif 400 < passed <= 500:
            game.screen.blit(self.image, self.rect)
        elif 500 < passed <= 600:
            game.screen.blit(self.image3, self.rect)
        elif 600 < passed <= 700:
            game.screen.blit(self.image2, self.rect)
        elif 700 < passed <= 800:
            game.screen.blit(self.image3, self.rect)
        elif 800 < passed:
            self.kill()
class Life(sprite.Sprite):
    def __init__(self, xpos, ypos):
        sprite.Sprite.__init__(self)
        self.image = IMAGES['ship']
        self.image = transform.scale(self.image, (23, 23))
        self.rect = self.image.get_rect(topleft=(xpos, ypos))

    def update(self, *args):
        game.screen.blit(self.image, self.rect)
class Text(object):
    def __init__(self, textFont, size, message, color, xpos, ypos):
        self.font = font.Font(textFont, size)
        self.surface = self.font.render(message, True, color)
        self.rect = self.surface.get_rect(topleft=(xpos, ypos))

    def draw(self, surface):
        surface.blit(self.surface, self.rect)
class SpaceInvaders(object):
    def __init__(self):

        mixer.pre_init(44100, -16, 1, 4096)
        init()
        self.clock = time.Clock()
        self.caption = display.set_caption('Space Invaders')
        self.screen = SCREEN
        self.background = image.load(IMAGE_PATH + 'background.jpg').convert()
        self.startGame = False
        self.mainScreen = True
        self.gameOver = False

        self.enemyPosition = ENEMY_DEFAULT_POSITION
        self.titleText = Text(FONT, 50, 'Space Invaders', WHITE, 164, 155)
        self.titleText2 = Text(FONT, 25, 'Press any key to continue', WHITE, 201, 225)
        self.gameOverText = Text(FONT, 50, 'Game Over', RED, 250, 270)
        self.gameOverText0 = Text(FONT, 50, 'G', RED, 250, 270)
        self.gameOverText1 = Text(FONT, 50, 'Ga', RED, 250, 270)
        self.gameOverText2 = Text(FONT, 50, 'Gam', RED, 250, 270)
        self.gameOverText3 = Text(FONT, 50, 'Game', RED, 250, 270)
        self.gameOverText4 = Text(FONT, 50, 'Game O', RED, 250, 270)
        self.gameOverText5 = Text(FONT, 50, 'Game Ov', RED, 250, 270)
        self.gameOverText6 = Text(FONT, 50, 'Game Ove', RED, 250, 270)
        self.nextRoundText = Text(FONT, 50, 'Next Round', WHITE, 240, 270)
        self.enemy1Text = Text(FONT, 25, '   =   10 pts', GREEN, 368, 270)
        self.enemy2Text = Text(FONT, 25, '   =   20 pts', BLUE, 368, 320)
        self.enemy3Text = Text(FONT, 25, '   =   30 pts', PURPLE, 368, 370)
        self.enemy4Text = Text(FONT, 25, '   =   ?????', RED, 368, 420)
        self.scoreText = Text(FONT, 20,'Score', WHITE, 5, 5)
        self.livesText = Text(FONT, 20,'Lives', WHITE, 640, 5)


        self.life1 = Life(715, 3)
        self.life2 = Life(742, 3)
        self.life3 = Life(769, 3)
        self.livesGroup = sprite.Group(self.life1, self.life2, self.life3)

    def reset(self, score):
        self.player = Ship()
        self.playerGroup = sprite.Group(self.player)
        self.explosionsGroup = sprite.Group()
        self.bullets = sprite.Group()
        self.mysteryShip = Mystery()
        self.mysteryGroup = sprite.Group(self.mysteryShip)
        self.enemyBullets = sprite.Group()
        self.make_enemies()
        self.allSprites = sprite.Group(self.player, self.enemies, self.livesGroup, self.mysteryShip)
        self.keys = key.get_pressed()

        self.timer = time.get_ticks()
        self.noteTimer = time.get_ticks()
        self.shipTimer = time.get_ticks()
        self.score = score
        self.create_audio()
        self.makeNewShip = False
        self.shipAlive = True

    def make_blockers1(self, number):
        barrierDesign = [[],
                         [0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0],
                         [0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],
                         [0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0],
                         [0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0],
                         [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
                         [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
                         [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
                         [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
                         [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
                         [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
                         [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
                         [1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1],
                         [1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1],
                         [1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1],
                         [1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1],
                         ]
        blockerGroup = sprite.Group()
        row = 0
        for b in barrierDesign:
            column = 0
            for b in b:
                if b != 0:
                    blocker = Blocker(3, RED, row, column)
                    blocker.rect.x = 60 + (200 * number) + (column * blocker.width)
                    blocker.rect.y = BLOCKERS_POSITION +25+ (row * (blocker.height))
                    blockerGroup.add(blocker)
                column += 1
            row += 1
        # ground
        for x in range(200):
            blocker = Blocker(4, RED, row, column)
            blocker.rect.x = x * blocker.width
            blocker.rect.y = 595
            blockerGroup.add(blocker)
            column += 1
            row += 1
        return blockerGroup

    #def make_blockers(self, number):
    #    blockerGroup = sprite.Group()
    #    for row in range(4):
    #        for column in range(9):
    #            blocker = Blocker(10, RED, row, column)
    #            blocker.rect.x = 50 + (200 * number) + (column * blocker.width)
    #            blocker.rect.y = BLOCKERS_POSITION + (row * blocker.height)
    #            blockerGroup.add(blocker)
    #    return blockerGroup

    def set4Blocker(self):
        # トーチカ(blockers)を4つ作る
        self.allBlockers = sprite.Group(self.make_blockers1(0),# left end 
                                        self.make_blockers1(1),# second from left
                                        self.make_blockers1(2),# third from left
                                        self.make_blockers1(3) # right end
                                        )


    def create_audio(self):
        self.sounds = {}
        for sound_name in ['shoot','shoot2','invaderkilled', 'mysterykilled','shipexplosion']:
            self.sounds[sound_name] = mixer.Sound(SOUND_PATH + '{}.wav'.format(sound_name))
            self.sounds[sound_name].set_volume(0.1)

        self.musicNotes = [mixer.Sound(SOUND_PATH + '{}.wav'.format(i)) for i in range(4)]
        for sound in self.musicNotes:
            sound.set_volume(0.1)


        self.noteIndex = 0

    def play_main_music(self, currentTime):
        if currentTime - self.noteTimer > self.enemies.moveTime *3:
            self.note = self.musicNotes[self.noteIndex]
            if self.noteIndex < 3:
                self.noteIndex += 1
            else:
                self.noteIndex = 0

            self.note.play()
            self.noteTimer += self.enemies.moveTime * 3

    @staticmethod
    def should_exit(evt):
        # type: (pygame.event.EventType) -> bool
        return evt.type == QUIT or (evt.type == KEYUP and evt.key == K_ESCAPE)

    def check_input(self):
        self.keys = key.get_pressed()
        for e in event.get():
            if self.should_exit(e):
                sys.exit()
            if e.type == KEYDOWN:
                if e.key == K_s:
                    if len(self.bullets) == 0 and self.shipAlive:
                        if self.score <= 1990: # 点数が1500点まで
                            bullet = Bullet(self.player.rect.x + 23, self.player.rect.y + 5, -1, 15, 'laser', 'center')
                            self.bullets.add(bullet)
                            self.allSprites.add(self.bullets)
                            self.sounds['shoot'].play()

                        elif 1990 <= self.score < 3500 : # 点数が1500点以に達すると弾が2つづつ発射できる
                            leftbullet = Bullet(self.player.rect.x + 8, self.player.rect.y + 5, -1, 15, 'laser', 'left')
                            rightbullet = Bullet(self.player.rect.x + 38, self.player.rect.y + 5, -1, 15, 'laser', 'right')
                            self.bullets.add(leftbullet)
                            self.bullets.add(rightbullet)
                            self.allSprites.add(self.bullets)
                            self.sounds['shoot2'].play()

                        elif 3500 <= self.score :
                            bullet = Bullet(self.player.rect.x + 23, self.player.rect.y + 5, -1, 15, 'laser', 'center')
                            self.bullets.add(bullet)
                            leftbullet = Bullet(self.player.rect.x + 8, self.player.rect.y + 5, -1, 15, 'laser', 'left')
                            rightbullet = Bullet(self.player.rect.x + 38, self.player.rect.y + 5, -1, 15, 'laser', 'right')
                            self.bullets.add(leftbullet)
                            self.bullets.add(rightbullet)
                            self.allSprites.add(self.bullets)
                            self.sounds['shoot2'].play()

                    if ROUND_NUM >= 6 and self.shipAlive:
                        bullet = Bullet(self.player.rect.x + 23, self.player.rect.y + 5, -1, 15, 'laser', 'center')
                        self.bullets.add(bullet)
                        leftbullet = Bullet(self.player.rect.x + 8, self.player.rect.y + 5, -1, 15, 'laser', 'left')
                        rightbullet = Bullet(self.player.rect.x + 38, self.player.rect.y + 5, -1, 15, 'laser', 'right')
                        self.bullets.add(leftbullet)
                        self.bullets.add(rightbullet)
                        self.allSprites.add(self.bullets)
                        self.sounds['shoot2'].play()

            if e.type == KEYUP:
                if e.key == K_d or K_a:# キーアップでplayerの横移動の加速速度vxを初期値に戻す
                    self.player.vx = SHIP_VX


    def make_enemies(self):
        enemies = EnemiesGroup(10, 5)
        for row in range(5):
            for column in range(10):
                enemy = Enemy(row, column)
                enemy.rect.x = 157 + (column * 50)
                enemy.rect.y = self.enemyPosition + (row * 45)
                enemies.add(enemy)

        self.enemies = enemies

    def make_enemies_shoot(self):
        if (time.get_ticks() - self.timer) > LEVEL and self.enemies:
            enemy = self.enemies.random_bottom()
            self.enemyBullets.add(Bullet(enemy.rect.x + 14, enemy.rect.y + 20, 1, 5, 'enemylaser', 'center'))
            self.allSprites.add(self.enemyBullets)
            self.timer = time.get_ticks()

    def calculate_score(self, row):
        scores = {0: 30, 1: 20,2: 20, 3: 10, 4: 10, 5: choice([50, 100, 150, 300])}
        score = scores[row]
        self.score += score
        return score

    def create_main_menu(self):
        self.enemy1 = IMAGES['enemy3_1']
        self.enemy1 = transform.scale(self.enemy1, (40, 40))
        self.enemy2 = IMAGES['enemy2_2']
        self.enemy2 = transform.scale(self.enemy2, (40, 40))
        self.enemy3 = IMAGES['enemy1_2']
        self.enemy3 = transform.scale(self.enemy3, (40, 40))
        self.enemy4 = IMAGES['mystery']
        self.enemy4 = transform.scale(self.enemy4, (80, 40))
        self.screen.blit(self.enemy1, (318, 270))
        self.screen.blit(self.enemy2, (318, 320))
        self.screen.blit(self.enemy3, (318, 370))
        self.screen.blit(self.enemy4, (299, 420))

    def check_collisions(self):
        sprite.groupcollide(self.bullets, self.enemyBullets, True, True)

        for enemy in sprite.groupcollide(self.enemies, self.bullets, True, True).keys():
            self.sounds['invaderkilled'].play()
            self.calculate_score(enemy.row)
            EnemyExplosion(enemy, self.explosionsGroup)
            self.gameTimer = time.get_ticks()

        for mystery in sprite.groupcollide(self.mysteryGroup, self.bullets, True, True).keys():
            mystery.mysteryEntered.stop()
            self.sounds['mysterykilled'].play()
            score = self.calculate_score(mystery.row)
            MysteryExplosion(mystery, score, self.explosionsGroup)
            newShip = Mystery()
            self.allSprites.add(newShip)
            self.mysteryGroup.add(newShip)

        for player in sprite.groupcollide(self.playerGroup, self.enemyBullets, True, True).keys():
            self.sounds['shipexplosion'].play()
            ShipExplosion(player, self.explosionsGroup)
            if self.life3.alive():
                self.life3.kill()
            elif self.life2.alive():
                self.life2.kill()
            elif self.life1.alive():
                self.life1.kill()
            else:
                self.gameOver = True
                self.startGame = False

            self.makeNewShip = True
            self.shipTimer = time.get_ticks()
            self.shipAlive = False

        if self.enemies.bottom >= 540:
            sprite.groupcollide(self.enemies, self.playerGroup, True, True)

            if not self.player.alive() or self.enemies.bottom >= 600:
                self.gameOver = True
                self.startGame = False

        sprite.groupcollide(self.bullets, self.allBlockers, True, True)

        bullet = sprite.groupcollide(self.enemyBullets, self.allBlockers, True, True)
        for enemylaser in bullet.keys():
            bulletExplosion(enemylaser, self.explosionsGroup)

        if self.enemies.bottom >= BLOCKERS_POSITION:
            sprite.groupcollide(self.enemies, self.allBlockers, False, True)

    def create_new_ship(self, createShip, currentTime):
        if createShip and (currentTime - self.shipTimer > 1600):
            self.player = Ship()
            self.allSprites.add(self.player)
            self.playerGroup.add(self.player)
            self.makeNewShip = False
            self.shipAlive = True

    def create_game_over(self, currentTime):

        # ゲームオーバーの文字を点滅
        passed = currentTime - self.timer
        if 900 < passed < 1000:
            self.gameOverText0.draw(self.screen)
        elif 1000 < passed < 1100:
            self.gameOverText1.draw(self.screen)
        elif 1100 < passed < 1200:
            self.gameOverText2.draw(self.screen)
        elif 1200 < passed < 1300:
            self.gameOverText3.draw(self.screen)
        elif 1300 < passed < 1400:
            self.gameOverText4.draw(self.screen)
        elif 1500 < passed < 1600:
            self.gameOverText5.draw(self.screen)
            #self.screen.blit(self.background, (0, 0))
        elif 1600 < passed < 1700:
            self.gameOverText6.draw(self.screen)
        elif 1700 < passed < 1800:
            self.gameOverText.draw(self.screen)
        elif passed > 4000:
            self.mainScreen = True # メイン画面へ

        for e in event.get():
            if self.should_exit(e):
                sys.exit()

    def main(self):
        while True:
            if self.mainScreen:
                self.screen.blit(self.background, (0, 0))
                self.titleText.draw(self.screen)
                self.titleText2.draw(self.screen)
                self.enemy1Text.draw(self.screen)
                self.enemy2Text.draw(self.screen)
                self.enemy3Text.draw(self.screen)
                self.enemy4Text.draw(self.screen)

                self.create_main_menu()
                for e in event.get():
                    if self.should_exit(e):
                        sys.exit()
                    if e.type == KEYUP:
                        # 最初のゲームスタート
                        # トーチカ(blockers)を作る
                       # self.allBlockers = sprite.Group(self.make_blockers1(0),# left end 
                       #                                 self.make_blockers1(1),# second from left
                       #                                 self.make_blockers1(2),# third from left
                       #                                 self.make_blockers1(3) # right end
                       #                                 )
                        self.set4Blocker() # トーチカ(blockers)を作る

                        self.livesGroup.add(self.life1, self.life2, self.life3)
                        self.reset(0)
                        self.startGame = True
                        self.mainScreen = False

            elif self.startGame:
                if not self.enemies and not self.explosionsGroup:
                    currentTime = time.get_ticks()
                    if currentTime - self.gameTimer < 3000:
                        self.screen.blit(self.background, (0, 0))
                        self.scoreText2 = Text(FONT, 20, str(self.score), GREEN, 85, 5)
                        self.scoreText.draw(self.screen)
                        self.scoreText2.draw(self.screen)
                        self.nextRoundText.draw(self.screen)
                        global ROUND_NUM
                        self.roundText = Text(FONT, 50, str(ROUND_NUM + 2), WHITE, 390, 350)
                        self.roundText.draw(self.screen)
                        self.livesText.draw(self.screen)
                        self.livesGroup.update()
                        self.check_input()
                    if currentTime - self.gameTimer > 3000:
                        # nextRound
                        ROUND_NUM += 1
                        # トーチカ(blockers)を作る
                        self.set4Blocker()

                        # move eneies closer to bottom
                        self.enemyPosition = ENEMY_DEFAULT_POSITION #+= ENEMY_MOVE_DOWN
                        self.reset(self.score)
                        self.gameTimer += 3000
                else:
                    currentTime = time.get_ticks()
                    self.play_main_music(currentTime)
                    self.screen.blit(self.background, (0, 0))
                    self.roundText = Text(FONT, 20, 'Round ' + str(ROUND_NUM + 1), WHITE, 325, 5)
                    self.roundText.draw(self.screen)
                    self.allBlockers.update(self.screen)
                    self.scoreText2 = Text(FONT, 20, str(self.score), GREEN, 85, 5)
                    self.scoreText.draw(self.screen)
                    self.scoreText2.draw(self.screen)
                    self.livesText.draw(self.screen)
                    self.check_input()
                    self.enemies.update(currentTime)
                    self.allSprites.update(self.keys, currentTime)
                    self.explosionsGroup.update(currentTime)
                    self.check_collisions()
                    self.create_new_ship(self.makeNewShip, currentTime)
                    self.make_enemies_shoot()

            elif self.gameOver:
                currentTime = time.get_ticks()
                # Reset enemy starting position
                self.enemyPosition = ENEMY_DEFAULT_POSITION
                self.create_game_over(currentTime)
                ROUND_NUM = 0
            display.update()
            self.clock.tick(60)

main loop

if __name__ == '__main__':
    game = SpaceInvaders()
    game.main()

インベーダーゲームをつくる。

youtu.be

manuke.hateblo.jp

インベーダーゲームをつくることを目標にして、pythonプログラムを学びます。前回からの続きです。

何もしないpythonプログラムコード。 gist.github.com

次は、4行目から

pygame.init()

pygameを初期化ですね。pygameの公式の解説を見てみます。

Pygame Tutorials - Import and Initialize — pygame v2.0.0.dev5 documentation

pygameで多くのことを行う前に、初期化する必要があります。 これを行う最も一般的な方法は、1回の呼び出しのみです。

pygame.init()

これにより、すべてのpygameモジュールが初期化されます。 すべてのpygameモジュールを初期化する必要はありませんが、これにより自動的に初期化されるモジュールが初期化されます。 各pygameモジュールを手で簡単に初期化することもできます。 たとえば、フォントモジュールのみを初期化するには、呼び出すだけです。

pygame.font.init()

pygame.init()で初期化するときにエラーが発生した場合、エラーが発生することに注意してください。このようなモジュールを手動で初期化すると、エラーが発生すると例外が発生します。初期化する必要のあるモジュールにはget_init()関数もあり、モジュールが初期化されている場合はtrueを返します。

DISPLAYSURF = pygame.display.set_mode((400, 300))

5行目は、ウィンドウの pygame.Surfaceオブジェクト を返すpygame.display.set_mode()関数の呼び出しです。 関数に2つの整数のタプル値を渡す。 このタプルは、ピクセル単位でウィンドウを作成する幅と高さをset_mode()関数に伝えます。(400, 300)は、幅400ピクセル、高さ300ピクセルのウィンドウを作成します。

単に2つの整数そのものではなく、2つの整数の タプル をset_mode()に渡していることに注意してください。関数を呼び出す正しい方法は、

pygame.display.set_mode((400, 300))

です。2つの整数値を()で囲んで、さらに関数の引数とするために()で囲んでいますね。

pygame.display.set_mode(400, 300)のような関数呼び出しは、次のようなエラーを引き起こします。

TypeError:引数1は、intではなく2アイテムのシーケンスでなければなりません。

変数DISPLAYSURF は幅400ピクセル、高さ300ピクセルpygame.Surfaceオブジェクト を指すことになります。

pygame.Surfaceオブジェクトってなんだ?

Surfaceオブジェクト1は、長方形の2D画像を表すオブジェクト。 Surfaceオブジェクトのピクセルは、Pygameの描画関数を呼び出して変更し、画面に表示できる。 ウィンドウの境界、タイトルバー、およびボタンは、Surfaceオブジェクトの一部ではない。 特に、pygame.display.set_mode()によって返されるSurfaceオブジェクトは、ディスプレイSurfaceと呼ばれます。 ディスプレイのSurfaceオブジェクトに描画されるものはすべて、pygame.display.update()関数 が呼び出されたときにウィンドウに表示されます。

Surfaceオブジェクトをコンピューターのモニター画面に描画するよりも、Surfaceオブジェクト(コンピューターのメモリにのみ存在する)に描画する方がはるかに高速です。

pygame.dislpay.set_caption('Hello World')

pygame.display.set_caption()関数 によってウィンドウのトップにキャプションテキストをセットします。'Hello World'の文字列の値が、関数の呼び出しに渡されてテキストがキャプションにつくられます。

f:id:honda-satoru:20191219045515p:plain
window001.py ウィンドウキャプション

game loop

ここでとり上げるゲームプログラムコードには、メインループ と呼ばれる絶えず繰り返されるループ命令のブロックがあります。

ゲームとはどのような処理をしているでしょうか。

多くのゲームでは、プレーヤーの体力と位置、敵の体力と位置、盤面に付けられたマーク、スコアなどが追跡されます。プレイヤーがダメージを受ける、敵がどこかに移動する、ゲーム世界で何かが発生するなど、何かが起こるたびに、ゲームの状態 が変化したと言います。 ゲームの状態 には変数の値が含まれていて、ゲームの状態の変化はゲームプログラム内のすべての変数の値のセットを参照することによって表現されるわけです。 保存できるゲームをプレイしたことがある場合、「保存状態」は保存した時点のゲーム状態です。

通常、ゲームの状態はイベント(マウスのクリックやキーボードの押下など)または時間の経過に応じて更新されるため、発生した新しいイベントを1秒間に何度も常にチェックおよび再チェックしています。 ゲームループは、これを受け持ちます。

メインループ内には、作成されたイベントを調べるコードがあります(Pygameでは、これは pygame.event.get()関数 を呼び出すことで行われます)。

メインループには、作成されたイベントに基づいてゲームの状態を更新するコードもあります。これは通常、イベント処理 と呼ばれます。

このプログラムの中のメインループを、 game loop ゲームループ と呼びます。

game loop は、大まかに分けると3つのことをしています。

  1. イベントを処理 .
  2. ゲームの状態を更新 .
  3. ゲームの状態を画面に描画 .

この3つがグルグルとずっと回り続けているというわけです。これがゲームループです。このゲームループというブロックがゲームのプログラムコードの中にあるということは重要です。

このゲームループは、7行目に現れてきます。

while True: #ゲームループのブロック
  for event in pygame.event.get(): # イベント処理ブロック
    if event.type == QUIT:  # ユーザーからのプログラム終了のイベントを受け取ったら
      pygame.quit() # pygameの終了
      sys.exit() # プログラムを終了させる
    pygame.display.update() # ゲームの状態を画面に描画

ユーザーがキーボードキーを押す、プログラムのウィンドウ上でマウスを動かすなど、いくつかのアクションのいずれかを行うと、pygame.event.EventオブジェクトPygameライブラリによって作成され、 イベント として記録されます。(これは、pygameモジュールの中の、eventモジュールに存在するEventというタイプのオブジェクト。)pygame.event.get() 関数を呼び出して、どのイベントが発生したかを調べることができます。 pygame.event.Eventオブジェクト(単にEventオブジェクトと呼びます)。

Eventオブジェクトのリスト は、最後に呼び出された pygame.event.get()関数 から後に発生した各イベントごとにできます。(または、pygame.event.get() が一度も呼び出されていない場合、プログラムの開始以降に発生したイベントが Eventオブジェクトのリスト になります。)

ここで リスト(Lists) という概念がでてきました。さきほど、タプル(Tuples) というのも出てきました。タプルの方は、(400, 300)というかたちでした。

リストは、 配列 と呼ばれる別のデータ構造に似ています。 リストのサイズは変更できますが、配列は変更できません。

IDLEもしくは、spyderを使っている場合、IPythonコンソールでコマンドラインを使ってリストを作って確認することが出来ます。

f:id:honda-satoru:20191219064645p:plain
IPythonコンソール

>>> x = [1,2]
>>> print(x)
[1, 2]

リスト内の個々の要素をprintするには

>>> print(x[0])
1

f:id:honda-satoru:20191219073151p:plain
list プリント

アイテムの場所を含むこの番号は、 インデックス と呼ばれます。 リストの場所はゼロから始まります。10個の要素を持つリストまたは配列にはインデックス10番番目[10]に要素がありません。 [0]から[9]で10個です。10個のアイテムのリストを作成してからインデックス10番を持たないのは、ほとんどのコンピューター言語で、1ではなく0でカウントを開始するのと同じです。 数字のリストを操作する際に考慮すべき数字のセットは2つあることを憶えておいてください。位置(インデックス)と値です。インデックスとも呼ばれる位置は 、値がある場所を指します。値は、その場所に保存されている実際の数値です。

window001.pyにもどって、8行目

for event in pygame.event.get():

このforブロックは、pygame.event.get() によって返された Eventオブジェクトのリスト を反復処理するforループです。最後の行の pygame.display.update() までが、このforのブロックに含まれます。

forループの各反復で、event という名前の変数に、このリスト内の次のイベントオブジェクトの値が割り当てられます。 pygame.event.get() から返されるイベントオブジェクトのリストは、イベントが発生した順になります。ユーザーがマウスをクリックしてキーボードキーを押すと、マウスクリックのイベントオブジェクトがリストの最初の項目になり、キーボードを押すイベントオブジェクトが2番目になります。イベントが発生していない場合、pygame.event.get() は空のリストを返します。

インベーダーゲームをつくる。

pythonでゲームをつくっていくのに、pygame1というゲームに特化したモジュールをOSにインストールする。 これは、PCのオーディオデバイスにアクセスしたり、キーボードの入力を検知したり、画面の描画の処理をしたりする部分をモジュール化したライブラリで負担して、コーディングする方としては、ライブラリの用意した命令でプログラムすれば、ゲームの動作に必要なたくさんの処理を列挙して記述する必要が無いということ。

ここで、pythonでのゲームつくりの本である Making Games with Python and Pygame Paperback – 2012 by Al Sweigart から第2章– PYGAMEの基本を引用しよう。

Pythonにプログラムに追加機能を提供するrandom、math、timeなどのモジュールが付属するように、Pygameフレームワークには、グラフィックスの描画、サウンドの再生、マウス入力の処理などの機能を備えたいくつかのモジュールが含まれます。

Software developer. UI designer. Tech book author. Al Sweigart2のウェブサイト https://inventwithpython.com/pygame/chapter2.html

ここではPygameフレームワーク3とされている。 まずフレームワークで、ゲームのプログラムを見てみようか。 ここで言うゲームのプログラムは、先にあげたインベーダーゲームのプログラム4を含む共通の構造を有するもので、つまり似たようなお決まりがあるものを指す。

ゲームのプログラムの構造

いったんインベーダーゲームから離れて、Pygameモジュールを活用したフレームワークについて、ゲームのプログラムの構造の理解、また今のところはインベーダーゲームのプログラムについて全くわからないけれども、構造から理解すると、どこにどういう処理が書かれているのかまでは推理できるようになるというわけだ。

manuke.hateblo.jp

では、ゲームプログラムの最小の要件をそなえたプログラムを読もう。 いや、ウィンドウプログラムの要件というべきだ。 つまり、モニター上にウィンドウが表れて、ウインドウが閉じることが出来るプログラム。ウィンドウを閉じるとプログラムは終了する。これって、大事なことだ、ほんとに。 無事に閉じて終了できないプログラムなんて悪夢でしょう。

window001.py

import pygame, sys
from pygame.locals import *

pygame.init()
DISPLAYSURF = pygame.display.set_mode((400,300))
pygame.display.set_caption('Hello world')
while True:
  for event in pygame.event.get():
    if event.type == QUIT:
      pygame.quit()
      sys.exit()
    pygame.display.update()

さて、ここから、pythonコードを実行していくので、pythonコードを書けるエディターと、pythonを実行するコンパイラが必要になってくる。 いろんな組み合わせがあるようだけれども、デバッガ5というプラグラムコードを実行しながら、プログラムがどうやって実行されているのかチェックできるプログラムが必要になった場合はspyderというプログラムをオススメしておきます。6

もちろんいろんな考えや経験があるので、他のものを使ってもいい。 sublime text3でも、atomでもVSCodiumでもIDLEでも、好きなのをどうぞ。他にもある7。 いろいろな分け方があるわけだけども、オープンソースのものと、そうでないものということで分けると、

オープンソース
  • spyder(エディターとデバッカ、またspyderを含むパッケージにpythonコンパイラも含まれているので総合開発環境といえる。)
  • IDLE(エディターとデバッカ、総合開発環境。オープンソース??)
  • atomテキストエディター。それぞれのパッケージをインスールすることでpythonシンタックスハイライトや、デバッカー機能も付け足すことができる。これとは別にpythonのインストールが必要。)
  • VSCodiumテキストエディター。それぞれのパッケージをインスールすることでpythonシンタックスハイライトや、デバッガ機能も付け足すことができる。これとは別にpythonのインストールが必要。)
  • Vimテキストエディター。それぞれのパッケージをインスールすることでpythhonのデバッガ機能も付け足すことができる。これとは別にpythonのインストールが必要。キーバインドが特殊ではあるけれどキータイプするには便利な機能がある。)

オープンソースではない
  • sublime text3テキストエディター。それぞれのパッケージをインスールすることでpythhonのデバッガ機能も付け足すことができる。これとは別にpythonのインストールが必要。)
  • pycharm(エディターとデバッガ、総合開発環境。)

つまり、テキストエディターとpythonコンパイラという組み合わせで、好みのテキストエディターを使うのと、総合開発環境に用意されたエディターを使うかということだけれど、spyder は、後者で、メリットとしてはデバッガが扱いやすいという印象がある。 spyder を選ぶ場合は、pythonのインストールもspyderを含むパッケージでインストールすることが推奨されています。 やや、ややこしいが、AnacondaWinPython などのパッケージで spyder を含む構成でインストールすることができる。

また、上記の全てのpythonの実行できる環境と、また別にPygameもインストールする必要がある。PygameGNU Lesser General Public Licenseというstrong copyleft8哲学のもとでのライセンスで配布されています。何のことかわからなくてもいいですが、調べてみると興味深いかもしれません。 どれを選ぶにしても、それぞれ試してみるのには時間がかかると思います。じっくり、ゆっくり吟味してください。

さて、さっきでてきた、pythonのプログラムコード’window001.py’を実行するとこんな感じになりました。

f:id:honda-satoru:20191218223957p:plain
window001.py 実行画面

実行までできたけど、こうならないという場合は、エラーがないかよくみてください。 まず、あまりまだきちんとふれていませんが、このプログラムはpythonの他にpygameというモジュールを使っています。 ですから、pygameというモジュールをインストールする必要があります。

PyGameについて簡単に説明

pygameは時間、ビデオ(静止画像とビデオの両方)、音楽、フォント、さまざまな画像形式、カーソル、マウス、キーボード、ジョイスティックなどを処理できます。 PyGameSDL9に基づいています。SDLクロスプラットフォームであり、非常にシンプルなCライブラリ。ただし、PyGameは単なる単純なラッパーではなく、PyGame独自の機能も追加するため、ゲームの作成はさらに簡単になります。 Pygameのインストールについては、「How to Install Pygame」というキーワードでインターネットで調べてみてください。 全くわからないという場合は、youtubeで5分くらいの動画で調べてみてください。英語での解説でわからないという場合でも、5分くらいでかなり丁寧にステップバイステップで説明されているものがありますから、ことばがよく解らない場合でも、画面をよーく見て、一通り見終わったら、解らないところを見直してキーワードを抜き出して、またインターネットで検索してみてください。

実に遠回りに思えるかもしれないですが、まったく解らないことを、探索する場合に、まず一通り見るというのは重要なことです。知らないことを解るということは、ありえませんから、一度見て解らなくてもそれは、当然です。初めて一通り見たとき、ここが解らないということを知ることができます。

また、できれば、1つだけ見るのではなくて、いくつかのケースをピックアップして、複数を見てみてください。それぞれに全くおなじではなく、差があると思います。 その差も、いくつか見ておいて、なんか違うなと心にとどめておけば、また随分後になって、役に立つことがあります。今はわからないことでも、自分がそのレベルに達したときに、初めて、あれはこの状況で活かされてることだったんじゃないかな?と気がつくときがあります。

インターネット上には様々なレベルの情報があって、目的のレベルの情報を拾い上げることが重要だと思います。 それには、ある程度時間をかけて、一見無駄な情報も取り込んで考えてみるという積み重ねが必要です。最初は遠回りですが、そこを通らないと、延々と何時までも遠回りが続くと考えることもできます。 そのような道筋を数回通ると、徐々に必要な情報にたどり着きやすくなってくるはずです。

とはいえ、Pygameのインストールは難しくはなさそうです。情報はたくさんあるので吟味してインストールしてみてください。

さて、先ほどのpythonのプログラムのの動作を確認します。

f:id:honda-satoru:20191219013120p:plain
window001.py プログラム終了
プログラムコードの最初の2行を見てみます。

import pygame, sys
from pygame.locals import *

これはどういう意味か。 pygameはモジュールでした。sysは初めて登場します。 これは、importステートメントと呼ばれる構文です。モジュールをプログラムに読み込んで、モジュール内にある関数などをプログラムから使えるようにするわけです。

import モジュール名,モジュール名,... と使用します。 sysは、モジュールですがPygameのように別途インストールする必要はないモジュールですがプログラムコードで扱うのでimportステートメントで読み込みます。

2行目は、同じくimportステートメントですが、通常、モジュール内の関数を呼び出す場合は、モジュールのインポート後に module名.function名()を使用する必要があります。 from modulename import * を使用すると、モジュール名をスキップして単にfunction名()というように使用できます。 つまりここでは、from pygame.locals import * ですから、pygameというモジュールの中の関数などをプログラムコードで使いたいときに、通常はpygame.function名()と表記するところを、function名()でpygame内の関数を呼び出せるということです。


  1. Pygameのウェブサイト https://www.pygame.org/

  2. https://alsweigart.com/

  3. ソフトウェアフレームワーク - Wikipedia

  4. spaceinvaders_copy.py · GitHub

  5. デバッガ - Wikipedia

  6. テキストエディターにpython向けのデバッガのパッケージを追加でインストールして、プログラムを実行するとき、どうしてエラーになるのかデバッグします。デバッガは、プログラムが落ちても、デバッガがそれを監視しているというものなのだと思います。総合開発環境のspyderをどういうものか見てみると、デバッガのなかでエディターが動いているような感じでした。これは、プログラムの流れを見ながら勉強していくには都合がいいように思えます。プログラムコード自体を比較して見たいときには、テキストエディターを使い、プログラムの実行を処理の順を追って可視化したいときには、spyderなどのプログラムを使うというように分けて使いたいと思います。テキストエディターにもプラグインのような形で対応するデバッガが配布されているので、好みのテキストエディターに使いやすいデバッガがあれば、そちらを使ってみてください。

  7. Top Python IDEs for 2019 (article) - DataCamp

  8. コピーレフトって何? - GNUプロジェクト - フリーソフトウェアファウンデーション

  9. SDLウェブサイト https://www.libsdl.org/

インベーダーゲームをつくる。

インベーダーゲームについて

インベーダーゲームってどんなゲームだっただろう?

1978年から、数々「スペースインベーダーズ」というゲームが世に出たはずだが、リアルタイムに1978年には知らないので、それ以降にどこかで見たものをぼんやり憶えていて、それをインベーダーゲームと思っているわけだが、実際に1978年のインベーダーゲームを見ることは難しい。 おそらくエミュレーターを使ってyoutubeにゲームプレイ画面をアップロードしている人が世界中にいるので、そのいくつかを見ておいて、インベーダーゲームのイメージをアップデートしておきたい。

www.youtube.com

おそらくAtari 2600に移植されたSpace Invaders(1980年)ではないかと思われるゲーム映像。 YouTube 効果音の相乗効果で演出されて、迫力がある。ザッザッザと迫ってくる音が、こわい。ブルータルなデスメタルのような世界感がある。 ゲーム性を感じる。アップロードした人に尋ねると、Stellaというエミュレーターによってプレイされている映像だそうだ。 stella-emu.github.io The Arcade Learning Environment (ALE) gym.openai.com

Atari社のゲームは、AI(強化学習)のデモンストレーション(Playing Atari with Deep Reinforcement Learning 2013 Volodymyr Mnih, Koray Kavukcuoglu, David Silver, Alex Graves, Ioannis Antonoglou Daan Wierstra, Martin Riedmiller, DeepMind Technologies)に使われるようになって、40年近く経て復活している。Atari伝説(USAのどこかに倒産したAtari社のゲーム機が埋められている)を追ったドキュメンタリーも近年あった。

ビデオゲームの墓場 - Wikipedia

www.youtube.com

古いインベーダーゲームの映像を見比べていくと、自分のおぼろげな記憶のインベーダーゲームに適合するのはApple Invaderだった。

YouTube

トーチカの破壊されていく描写に、角砂糖が削られていくような儚さを感じる。また、地表を守っているというコンセプトを継承して地表が描画されていて、地表にも痛々しい破壊が進んでいくことで荒廃感が演出されている。

いろいろなインベーダーゲームを見て、いま、現段階でのpythonコードにない特徴を確認する。

  • 地表があり。地表に攻撃の跡が残る。
  • 自機の放った弾が、画面上部のエリアで破裂する。
  • 自機に受けた攻撃で、爆発する描写。
  • エイリアンが放つ攻撃の弾がウネウネと波打っている。
  • プレイスタートでエイリアンの登場のとき演出がある。
  • エイリアンが1つだけになったとき、動きがものすごく速くなる。
  • 高得点を記録することが出来る。

など。 これらの演出が、ゲーム性につながっていると思われる。 ただのプログラムコードと、ゲーム性をそなえたプログラムコードという差がある。 そこは実際にプレイする側に置いているか、AIに学習させるためにゲームを持ってくるとか、自分の能力の確認のためにゲームプログラムのコードを書くといった目的との差なのかもしれない。 今のところ、用意したインベーダーゲームpythonコードには、ゲーム性を感じない。

ゲームのpythonコードの構造と、プログラミング方法がつかめれば、ゲーム性を増す演出はおいおいつけていけるだろう。

ということで、ゲームにおいてのpythonコードの構造を見ていくことにする。

Pythonでのプログラムの最初。インベーダーゲームをつくる。

Pythonを勉強始めるにあたって、全くわからないところからのスタートなので、「インベーダーゲームのつくり方」を理解することを目標に、やっていくうちにわかるだろうということを記していく。

f:id:honda-satoru:20191212235354p:plain
figure 001: window

まず、数日かけてpythonで書かれたインベーダーゲームソースコードを検証した。自分のイメージするインベーダーゲームに近いコードは1つしか見つけることが出来なかった。

github.com

人がインベーダーゲームをイメージしたとき、それはいろんな感性があり、違いがあるが、今回その要件としたことは、以下だ。

・トーチカ(エイリアンとプレーヤーの間にある防護壁のようなもの。)がある。また、トーチカは、エイリアンの攻撃によっても、プレーヤーの攻撃によっても弾との当たり判定によって、段階的に破壊される。また、エイリアンと接触しても破壊される。

・エイリアンが横動きする時、画面の端まで移動する。エイリアンの数が減れば、最終的にどのエイリアンが1つ残っても、この一つが、画面の端から端まで移動しつつ下降してくる。

この2つだけが要件だが、これを満たすソースコードを探して検証したところ、1つしかなかった。とりあえず、動作だけ確認していく過程で、解ったこともある。プログラムコードを書く(コピー&ペーストです)エディター、pythonコードの実行のさせ方、それはつまり開発環境というものだ。これは、後からまとめることにする。

このソースコードから実行しているインベーダーゲームが動作しているのは、このような感じ。作成者によってyoutubeにアップロードされている。

www.youtube.com

何日かかけていくつかのコードを読んで、全体に目を通すことによって、最終的に選んだプログラムのコードの中に、python特有の構文、オブジェクトオリエンテッドのクラス継承、スタティックメソッドなど含まれていて、このコードを見ていくことでpythonの学習は一通りできそうだと感じた。かなりきれいにコーディングされている(洗練されているというべきか)ので、逆にpython初めての自分には、わかりにくいわけだが、5年以上前に公開されているにもかかわらず作者によってコードが推敲されていて、python3で正常に動作するとも含めて好都合なのでこれをベースに学んでいくことにする。

後ほど、このソースコードを元に編集していくが、コード自体をこの場で読めるように貼っておく。

そして、その後の過程を経て変形したのはこちら。 インベーダーゲームのつづき - 珈琲焙煎者の観察

spaceinvaders_copy.py

gist.github.com

ファイルの用意

一つ空っぽのフォルダを用意して、その中に上記のソースコードファイルと、 fonts, sounds, images という3つのフォルダを作り、

fontsフォルダの中には、 以下のURLからダウンロードしたフォントファイルを収納する。 https://github.com/leerob/Space_Invaders/blob/master/fonts/space_invaders.ttf

soundsフォルダの中には、 以下のURLからすべてのサウンドファイルをダウンロードして収納する。 https://github.com/leerob/Space_Invaders/tree/master/sounds

imagesフォルダの中には、 以下のURLからすべての画像ファイルをダウンロードして収納する。 https://github.com/leerob/Space_Invaders/tree/master/images

これらの収まったフォルダは、SpaceInvadersという名前にすることにする。

画面の位置やサイズ、Figure 002、Figure 003のようになる。

f:id:honda-satoru:20191214014236p:plain
Figure 002

f:id:honda-satoru:20191213034136p:plain
Figure 003

ここまでで、「こういうものをつくりたい」と、そういうこと。材料はそろったので、ではどういう風に考えられて、上記のコードになったのか勉強していく。

スーパーコライダーの優しい紹介(第2版)

f:id:honda-satoru:20191021020623p:plain

A Gentle Introduction to SuperCollider

これは、Bruno Ruviaroが発行した非常に優れたチュートリアル本「SuperColliderの優しい紹介(第2版)」を日本語に翻訳したものです。(作業中)

works.bepress.com

1569163432316.png この作品はクリエイティブ・コモンズの下でライセンスされています Attribution-ShareAlike 4.0 International ライセンス https://creativecommons.org/licenses/by-sa/4.0/deed.ja

SuperColliderの優しい紹介

著者 ブルーノ・ルヴィアロ

翻訳 ねことほんだな

[TOC]


SuperColliderの優しい紹介

ブルーノ・ルヴィアーロ

First published in 2014. This revision is from November 20, 2015.

2015年11月20日


Part I

BASICS

1 Hello World

最初の SuperCollider プログラムを作成する準備はできましたか? SCが稼働していると仮定します。

あなたの前で、新しいドキュメントを開き(menu File→New, もしくはショートカット [ctrl+N]) 、次の行を入力します。

"Hello World".postln;

その行の任意の場所にカーソルを置きます(開始、中間、終了のいずれでもかまいません)。[ctrl + Enter]を押してコードを評価します。“Hello world” がPost ウィンドウに表示されます。おめでとうございます!それがあなたのはじめての SuperCollider プログラムでした。

Figure01.png 図1:SuperCollider IDEインターフェース。

ヒント:bird2.PNG
このドキュメント全体を通して、ctrl(コントロール)は、LinuxおよびWindowsプラットフォームで使用されるキーボードショートカットのmodifierキーを示します。Mac OSXでは、代わりにcmd(コマンド)を使用します。

図1は、最初にSuperCollider IDE(統合開発環境)を開いたときのスクリーンショットです。少し時間をかけて理解していきましょう。

SuperCollider IDEとは何ですか? これは、"SuperCollider専用に開発されたクロスプラットフォームのコーディング環境(...)であり、使いやすく、経験豊富なコーダー向けの強力な機能が散りばめられています。また、非常にカスタマイズ可能です。それはMac OSX、LinuxWindowsでも同様に動作します。"*

SCウィンドウに表示される主な部分は、コードエディター、ヘルプブラウザー、およびPost ウィンドウです。SuperColliderを開いたときにこれらのいずれも表示されない場合は、メニューに移動します。

View→Docklets (そこから各ドックレットを表示または非表示にできます)。ステータスバーもあり、常にウィンドウの右下隅にあります。そこにプリントされているものすべてをまだ理解していない場合でも、常に Post ウィンドウを表示したままにします。Post ウィンドウには、コマンドに対するプログラムの応答(コード評価の結果、さまざまな通知、警告、エラーなど)が表示されます。

ヒント:bird2.PNG
ショートカット [Ctrl ++] および [Ctrl +-] でエディターのフォントサイズを一時的に拡大および縮小できます(それぞれコントロールキーとプラスキーまたはマイナスキー)。実際のプラスキーを持たないラップトップを使用している場合は、[Ctrl + shift + =] を使用します。

2 サーバーと言語

ステータスバーには、" Interpreter "と"Server"という言葉が表示されます。Interpreterはデフォルトでオン("Active")になり、"Server"はオフになります(すべてゼロが意味します)。Interpreterとは何ですか?Serverとは何ですか?

SuperCollider は実際には、サーバーと言語の2つの異なるアプリケーションで構成されています。サーバーは、音を出す責任があります。言語(クライアントまたはインタープリターとも呼ばれる)は、サーバーの制御に使用されます。最初は scsynth(SC-synthesizer) と呼ばれ、2番目は sclang(SC-language) と呼ばれます。ステータスバーには、これら2つのコンポーネントのステータス(オン/オフ)が表示されます。


*SuperCollider ドキュメントから引用: http://doc.sccode.org/Guides/SCIde.html IDEインターフェースの詳細については、そのページをご覧ください。

この区別が今あなたにとってあまり意味をなさない場合でも心配しないでください。この時点で知っておく必要がある2つの主なものは次のとおりです。

  1. SuperCollider に入力するすべてのものは、SuperCollider 言語(クライアント)で作成されます。ここでコマンドを記述および実行し、Postウィンドウに結果を表示します。

  2. SuperColliderサウンドを生成するものはすべて、サーバーから送られます。つまり、いわば"サウンドエンジン"です、あなたによって SuperCollider 言語を通じてコントロールされます。

2.1 サーバーの起動

"Hello World"プログラムは音を出しませんでした。すべては言語で行われ、サーバーはまったく使用されませんでした。次の例では音が出ますので、サーバーが稼働していることを確認する必要があります。

サーバーを起動する最も簡単な方法は、ショートカット [ctrl + B] を使用することです。または、ステータスバーのゼロをクリックすることもできます。メニューがポップアップし、オプションの1つが"Boot Server"です。サーバーが起動すると、Post ウィンドウにアクティビティが表示されます。サーバーを正常に起動すると、ステータスバーの数字が緑色に変わります。SCを起動するたびにこれを行う必要がありますが、セッションごとに1回だけです。

3 最初の正弦波

"Hello World" は伝統的に、新しいプログラミング言語を学ぶときに人々が作成する最初のプログラムです。SuperCollider で既にそれを行っています。

単純な正弦波の作成は、コンピューター音楽言語の "Hello World" かもしれません。すぐにジャンプしましょう。次のコード行を入力して評価します。注意してくだい。これは大きな音になる場合があります。ボリュームを完全に下げ、ラインを評価してから、ゆっくりボリュームを上げます。

{SinOsc.ar}.play;

それは美しく、滑らかで、連続的で、おそらく少し退屈なサイン波です。[ctrl +.] でサウンドを停止できます(コントロールキーとピリオドキーです)。このキーの組み合わせを覚えておいてください。SCのすべてのサウンドを停止するために頻繁に使用するためです。次に、この正弦波をもう少し面白くしましょう。これを入力してください:

{SinOsc.ar(LFNoise0.kr(10).range(500, 1500), mul: 0.1)}.play;

行内の任意の場所にカーソルを置いて、[ctrl + Enter]を 押すだけで評価できます。または、評価する前に行全体を選択することもできます。

ヒント:bird2.PNG
コード例を自分で入力することは、優れた学習ツールです。自信をつけ、言語に慣れるのに役立ちます。デジタル形式のチュートリアルを読むとき、サンプルから短いコードスニペットをコピーして貼り付けたいと思うかもしれません。それで問題ありませんが、自分で入力すればもっと学習できます。少なくともSC学習の最初の段階で試してみてください。

4 エラーメッセージ

最後の例を評価しても音がしませんか? その場合、コードにタイプミスがあった可能性があります。間違った文字、カンマや括弧の欠落などです。コードに問題が発生すると、Post ウィンドウにエラーメッセージが表示されます。エラーメッセージは長くて不可解なことがありますが、パニックにならないでください。時間の経過とともにエラーメッセージの読み方を学びます。短いエラーメッセージは次のようになります。

ERROR: Class not defined.
    in file ’selected text’
    line 1 char 19:
    {SinOsc.ar(LFNoiseO.kr(12).range(400, 1600), mul: 0.01)}.play;
-----------------------------------
nil

このエラーメッセージは、"クラスが定義されていません"と表示され、エラーのおおよその場所("行1文字19")を指します。SCのクラスは、大文字で始まる青い単語(SinOsc や LFNoise0 など)です。このエラーは、ユーザーが最後に大文字の"O"を付けて LFNoiseO を入力したことが原因であることがわかりました。正しいクラスは LFNoise0 で、末尾に数字のゼロがあります。ご覧のとおり、細部への注意が重要です。

コードにエラーがある場合は、校正して、必要に応じて変更し、修正されるまで再試行してください。最初にエラーが発生していなかった場合は、エラーメッセージがどのように表示されるかを確認できるように、今すぐ導入してみてください(たとえば、コンマを削除します)。

ヒント:bird2.PNG
SuperCollider の学習は、ドイツ語、ポルトガル語、日本語などの別の言語を学習するようなものです。そのまま話そうとし、語彙を増やし、文法と構文に注意を払い、間違いから学びましょう。ここで起こり得る最悪の事態は、SuperCollider をクラッシュさせることです。サンパウロで間違ったバスに乗るほど悪くはありませんが、道順の要求が誤解されています。

5 パラメーターの変更

SuperCollider Bookの最初の章から改作された素晴らしい例があります。* 前の例と同様に、すべてを理解しようとして心配する必要はありません。音の結果を楽しんで、数字で遊んでください。

 {RLPF.ar(Dust.ar([12, 15]), LFNoise1.ar([0.3, 0.2]).range(100, 3000), 0.02)}.play;

サウンドを停止し、いくつかの数値を変更して、もう一度評価してください。たとえば、数字12と15を1と5の間の小さい数字に置き換えるとどうなりますか? LFNoise1 の後、0.3と0.2の代わりに1と2のようなものを試してみたらどうでしょうか? 一度に1つずつ変更します。新しいサウンドを以前のサウンドと比較し、違いを聞いてください。何が何を支配しているかを理解できるかどうかを確認してください。これは、SuperCollider を探索する楽しい方法です。何か面白いものを作成するコードスニペットを取得し、パラメーターをいじってバリエーションを作成します。すべての数字の役割を完全に理解していなくても、興味深い音の結果を見つけることができます。

ヒント:bird2.PNG
他のソフトウェアと同様に、[ctrl + S]を使用して作業内容を頻繁に保存してください。このようなチュートリアルで作業する場合、提供されているサンプルを使用して実験することで、興味深いサウンドを思いつくことがよくあります。好きなものを残したい場合は、コードを新しいドキュメントにコピーして保存します。すべての SuperCollider ファイルには、"SuperCollider Document"を表す拡張子.scdがあります。

*Wilson,S.とCottle,D.とCollins、N.(編集者)。SuperCollider Book,MIT Press,2011,p. 5.

チュートリアルのいくつかの内容は、SuperCollider Book の最初の章であるDavid Cottle の優れた"Beginner’s Tutorial"から借用、改作、またはインスピレーションを受けています。このチュートリアルでは、Cottle の章からいくつかの例と説明を借用しますが、それとは異なり、コンピューター音楽への露出が少ないと想定し、教育的アプローチのバックボーンとしてパターンファミリーを紹介します。

6 コメント

コード内の赤色で表示されるテキストはすべてコメントです。プログラミング言語を初めて使用する場合、コメントは、自分自身と後で読む必要のある人にとって、コードを文書化するのに非常に便利な方法です。二重スラッシュで始まる行はコメントです。有効なコード行の直後にコメントを書くことができます。評価の際、コメント部分は無視されます。SCでは、セミコロンを使用して有効なステートメントの終わりを示します。

2 + 5 + 10 − 5; // ただ計算するだけ

rrand(10, 20);   // 10~20の乱数を生成します

カーソルがその行の後のコメントの途中にある場合でも、行を評価できます。コメント部分は無視されます。次の2つの段落は、例のためだけに"コメント"として記述されます。

//ショートカット[ctrl + /]を使用して、1行のコードをすばやくコメントアウトできます。

"Some SC code here...".postln;

2 + 2;

//本当に長いコメントを書くと、テキストは、二重スラッシュで*開始*されない新しい行のように見える場合があります。それでも、コメントの1行としてカウントされます。

/*"スラッシュ+アスタリスク"を使用して、複数の行で長いコメントを開始します。
"アスタリスク+スラッシュ"で大きなコメントチャンクを閉じます。
上記のショートカットも大きなチャンクに対して機能します。コメントアウトするコードの行を選択し、[ctrl + /]を押します。コメント解除と同じです。*/

7 優先順位

SuperCollider は、操作に関係なく、左から右の優先順位に従います。これは、たとえば、乗算が最初に発生しないことを意味します。

// 高校では、結果は9でした。SCでは、14です。

5 + 2 * 2;

// 括弧を使用して、特定の操作順序を強制します。

5 + (2 * 2); // 9に等しい。

メッセージとバイナリ操作を組み合わせる場合、メッセージが優先されます。たとえば、5 + 2.squared では、平方が最初に発生します。

8 最後の post は常に post されます

小さくても役に立つ詳細:デフォルトで、SuperCollider は最後に評価されたものの結果を常にPostウィンドウにpostします。これは、評価時に Hello World コードが2度プリントされる理由を説明しています。新しいドキュメントに次の行を入力し、[ctrl + A] ですべてを選択して、すべての行を一度に評価します。

"最初の行".postln;
"2行目".postln;
(2 + 2).postln;
3 + 3;
"Finished".postln;

5行すべてが SuperCollider によって実行されます。明示的な postln 要求があったため、Postウィンドウに2 + 2の結果が表示されます。3 + 3 の結果が計算されましたが、postするリクエストがなかったため、表示されません。次に、最後の行のコマンドが実行されます( postln リクエストにより、"Finished" という単語がpostされます)。最終的に、最後に評価されたものの結果がデフォルトでpostされます。この場合、たまたま "Finished" という単語でした。

9 コードブロック

評価する前に複数行のコードを選択するのは面倒です。コードブロックを一度に実行するはるかに簡単な方法は、コードブロックを作成することです。一緒に実行するすべてのコード行をかっこで囲むだけです。以下に例を示します。

(
// A little poem
"Today is Sunday".postln;
"Foot of pipe".postln;
"The pipe is made of gold".postln;
"It can beat the bull".postln;
)

外側の括弧はコードブロックを区切ります。カーソルが括弧内の任意の場所にある限り、単一の [ctrl + Enter] がすべての行を評価します(上から下の順に実行されますが、非常に高速なので同時実行されているように見えます)。

コードブロックを使用すると、何かを変更して再評価するたびにすべての行を再度選択する必要がなくなります。たとえば、二重引用符で囲まれた一部の単語を変更し、変更した直後に [ctrl + Enter] を押します。すべての行を手動で選択することなく、コードブロック全体が評価されます。SuperCollider はブロックを1秒間ハイライトして、実行されている内容の視覚的なヒントを提供します。

10 Post ウィンドウをクリーンアップする方法

[ctrl + shift + P] これは、独自のセクションに相応しいフリークをクリーニングするのに非常に便利なコマンドでです: この行を評価して、後で[ctrl + shift + P] でPostウィンドウをきれいにしてください。

100.do({"この行を繰り返しptint...".scramble.postln});

どういたしまして。

11 SuperColliderの出力の記録

すぐに SuperCollider パッチのサウンド出力の録音を開始する必要があります。そのための簡単な方法を次に示します。

//クイックレコード
//記録を開始:
s.record;
//クールなサウンドを作成する
{Saw.ar(LFNoise0.kr([2,3]).range(100,2000),LFPulse.kr([4,5])* 0.1)}.play;  
//記録を停止する:
s.stopRecording;
//オプション:録音ボタン、ボリュームコントロール、ミュートボタンを備えたGUI:
s.makeWindow;

Post ウィンドウには、ファイルが保存されたフォルダーのパスが表示されます。ファイルを見つけて、Audacity または同様のプログラムで開き、サウンドが実際に録音されたことを確認します。詳細については、"サーバー"ヘルプファイルを参照してください( “Recording Support” までスクロールします)。または、オンラインで http://doc.sccode.org/Classes/Server.html

12 変数

数値、words、unit ジェネレーター、関数、またはコードブロック全体を変数に保存できます。変数は、1文字またはユーザーが選択した単語全体です。等号(=)を使用して変数を"割り当て"ます。これらの行を一度に1つずつ実行し、Post ウィンドウを監視します。

 x = 10;
 y = 660;
 y; // 内容を確認します
 x;
 x + y;
 y − x; 

最初の行は、変数xに数値10を割り当てます。2行目は、変数yに660を入れます。次の2行は、これらの文字がこれらの数字(データ)を"含む"ことを証明しています。最後に、最後の2行は、変数を使用してデータを操作できることを示しています。小文字のa~zは、SuperCollider の変数としていつでも使用できます。慣例により使用しない唯一の文字はsであり、これはデフォルトでサーバーを表します。何でも変数に入れることができます:

a = "Hello,World"; // 文字列
b = [0,1,2,3,5]; // リスト
c = Pbind(\note,Pwhite(0,10),\dur,0.1); // 後でPbindについてすべて学習します。心配しないでください

// ...そして元のデータを使用するのと同じように使用できるようになりました:
a.postln; // postする
b + 100; // 計算を行います
c.play; // そのPbindを再生します
d = b * 5; // bを取り、5を掛けて、それを新しい変数に割り当てます

多くの場合、変数に適切な名前を付けて、コード内で変数が何を表しているのかを思い出すのに役立ちます。~ (チルダ)を使用して、より長い名前の変数を宣言できます。チルダと変数名の間にスペースがないことに注意してください。

~myFreqs = [415,220,440,880,220,990];  
~myDurs = [0.1,0.2,0.2,0.5,0.2,0.1];  

Pbind(\freq,Pseq(~myFreqs),\dur,Pseq(~myDurs)).play;  

変数名は小文字で始まる必要があります。名前には、最初の文字としてではなく、数字、アンダースコア、大文字を使用できます。すべての文字は連続している必要があります(スペースや句読点は使用できません)。要するに、変数に名前を付けるときは、文字と数字、およびときどきアンダースコアを使用することし、他のすべての文字を避けてください。

~myFreqs,~theBestSineWave,および ~banana_3 は有効な名前です。

~MyFreqs,~theBest&*#SineWave,および ~banana!!! これらは悪い名前です。

作成できる変数には、"グローバル"変数とローカル変数の2種類があります。

12.1 "グローバル"とローカル

これまでに見た変数(単一の小文字aからz、およびチルダ(~)文字を含む)は、大まかに"グローバル変数"と呼ばれる場合があります。宣言されると、SuperCollider を終了するまで、パッチのどこでも、他のパッチでも、他のSCドキュメントでも"グローバルに"動作します。*


*技術的には、チルダで始まる変数は環境変数と呼ばれ、小文字の変数(a~z)はインタープリター変数と呼ばれます。SuperCollider の初心者は、これらの違いを心配する必要はありませんが、将来のために心に留めておいてください。SuperCollider Bookの第5章では、違いについて詳しく説明しています。

一方、ローカル変数は、行の先頭で予約キーワード var で宣言されます。宣言時に変数に初期値を割り当てることができます (var apples = 4) 。ローカル変数は、そのコードブロックのスコープ内にのみ存在します。

以下に、2種類の変数を比較する簡単な例を示します。行ごとに評価し、Post ウィンドウを監視します。

// 環境変数
~galaApples = 4;

~bloodOranges = 5;
~limes = 2;
~plantains = 1;

["Citrus", ~bloodOranges + ~limes ];  
["Non−citrus", ~plantains + ~galaApples ];  

// ローカル変数:コードブロック内でのみ有効。
// ブロックを1回評価して、Post ウィンドウを見る:
(
var apples = 4,oranges = 3,lemons = 8,bananas = 10;
["Citrus fruits", oranges + lemons].postln;  
["Non−citrus fruits", bananas + apples].postln;  
"End" .postln;
)

~galaApples; // まだ存在しています
apples; // なくなった

12.2 再割り当て

変数について理解する最後の便利な点は、変数を再割り当てできることです。いつでも新しい値を与えることができます。

// 変数を割り当てる
a = 10 + 3;
a.postln; // 確認してください
a = 999; // 変数を再割り当てします(新しい値を与えます)
a.postln; // チェックしてください:古い値はなくなりました。

初心者にとって混乱を招くことがある非常に一般的な方法は、変数自体が独自の再割り当てで使用される場合です。この例を見てください:

x = 10; // 変数xに10を割り当てます
x = x + 1; // 変数xにx + 1を割り当てます
x.postln; // 確認してください

最後の行を理解する最も簡単な方法は、

"変数 x の現在の値を取得し、それに 1 を追加し、この新しい結果を変数 xに割り当てる"

というように読むことです。

また、これがどのように役立つかについては後で説明します。*


*この例は、プログラミングでの等号が数学で学んだ等号とは異なることを明確に示しています。数学では、x = x + 1 は不可能です(数値をそれ自体に1を足すことはできません)。SuperCollider のようなプログラミング言語では、等号は一種のアクションと見なすことができます。記号の右側で式の結果を取得し、左側で変数に"代入"します。

Part II

パターン

13 パターンファミリー

新たにこれを試してみましょう。次のコード行を入力して実行します。

Pbind(\degree,Pseries(0,1,30),\dur,0.05).play;

13.1 Pbind に会う

Pbind は、SuperCollider の Pattern ファミリーのメンバーです。Pbind および Pseries の大文字Pは、パターン (Pattern) を表します。私たちはいずれすぐにファミリーの他のメンバーに会います。とりあえず、Pbind だけを詳しく見てみましょう。この単純な例を試してください:

Pbind(\degree,0).play;

このコード行が実際に行う唯一のことは、1秒間に1回、中央のCを演奏することです。キーワード \degree は音階を示し、数値0は音階のはじまりを意味します(Cメジャー音階が想定されているため、でだしのC音そのものです)。SuperCollider は1ではなく0からカウントを開始することに注意してください。上記のような単純な行では、ノートC、D、E、F、G ...は0、1、2、3、4 ...の数字で表されます。この数字を変更してみて、再評価したときに音がどのように変化するかを確認してください。負の数を使用して、中央のCの下の音符を選択することもできます(たとえば、-2を使用すると、中央のCの下に音符Aが表示されます)。要するに、ピアノの中央のC音が0であると想像してから、白鍵を上下に数えて(正または負の数)他の音を取得します。

次に、音符の長さを少し試してみます。Pbind はキーワード \dur を使用して、秒単位で期間を指定します。

Pbind(\degree,0,\dur,0.5).play;

もちろん、これは依然として非常に厳格で柔軟性がありません。常に同じ音で、常に同じ持続時間です。心配しないでください:物事はすぐに良くなります。

13.2 Pseq

スケールのように、いくつかの音を順番に演奏してみましょう。また、音符を短く、たとえば0.2秒にしましょう。

f:id:honda-satoru:20191104041736p:plain
\degree

Pbind(\degree,Pseq([0,1,2,3,4,5,6,7],1),\dur,0.2).play;

この行は、パターンファミリの新しいメンバーである Pseq を紹介しています。名前が示すように、このパターンはシーケンスを扱います。シーケンスを再生するために Pseq に必要なのは次のとおりです。

  • 角括弧で囲まれたアイテムのリスト

  • 多数の繰り返し。

この例では、リストは [0,1,2,3,4,5,6,7] で、繰り返し回数は1です。この Pseq は、単に"リストのすべての項目を順番に再生する"という意味です。これらの2つの要素、リストと繰り返し回数は、Pseq の括弧内にあり、コンマで区切られていることに注意してください。

Pbind 内の Pseq の位置にも注意してください。これは、\degreeの入力値です。これは重要です。最初の単純な Pbind のように、スケールの度合いに単一の固定数を提供する代わりに、一連の数のレシピである Pseq 全体を提供しています。これを念頭に置いて、このアイデアを簡単に拡張し、別の Pseq を使用して継続時間を制御することもできます。

Pbind(\degree,Pseq([0,1,2,3,4,5,6,7],5),\dur,Pseq([0.2,0.1,0.1,0.2,0.2,0.35],inf) ).play;

この例では何が起こっていますか?

最初に、最初の Pseq の繰り返し数を5に変更したため、スケール全体が5回再生されます。

次に、以前に固定した0.2の \dur 値を別の Pseq に置き換えました。この新しい Pseq には、6つのアイテムのリストがあります:[0.2,0.1,0.1,0.2,0.2,0.35]。これらの数値は、結果のノートのデュレーション値になります。

この2番目の Pseq の繰り返し値は "infinite." を表す inf に設定されます。これは、Pseq がシーケンスを繰り返すことができる回数に制限がないことを意味します。

Pbind は永遠にプレイしますか? いいえ:他の Pseq がジョブを終了した後、つまり一連のスケール度が5回再生された後に停止します。

最後に、この例には合計8つの異なるノート(最初の Pseq のリスト)がありますが、デュレーション(6番目の Pseq )には6つの値しかありません。このように異なるサイズのシーケンスを提供すると、Pbind は必要に応じて単純に循環します。

これらの質問に答えて、学んだことを実践してください。

  • 2番目の Pseq の repeats 引数として、inf の代わりに数値1を試してください。 何が起こるのですか?

  • この Pbind を永久にプレイするにはどうすればよいですか?

ソリューションは本の最後にあります。1

13.3 コードを読みやすくする

上記のコード行が非常に長いことに気づいたかもしれません。実際、技術的には単一のステートメントであるにもかかわらず、新しい行に折り返されるほど長い。長いコード行は読みにくいかもしれません。これを回避するには、コードをいくつかのインデントした行に分割するのが一般的です。

目標は、できるだけ明確でわかりやすいものにすることです。上記と同じ Pbind は、次のように書くことができます。

Pbind(
    \degree,Pseq([0,1,2,3,4,5,6,6,7],5),
    \dur,Pseq([0.2,0.1,0.1,0.2,0.2,0.35],inf)
).play;

これからは、このように Pbind を作成する習慣を身に付けてください。きちんと整理され、きちんと整理されたコードを書くことは、SuperCollider を学ぶ上で大いに役立ちます。

また、この Pbind を括弧で囲んでコードブロックを作成していることに注意してください(セクション9を思い出してください)。これは1行ではないため、すべてを一緒に実行するためにこれを行う必要があります。評価する前に、カーソルがブロック内のどこかにあることを確認してください。

13.4ピッチを指定する4つの方法

Pbind は、音度(主音との音程に従って、番号を振ったもの)だけでなく、ピッチを指定する他の方法も受け入れます。

  • 12個すべての半音符(ピアノの黒鍵と白鍵)を使用する場合は、\degree の代わりに\note を使用できます。0はミドルC を意味しますが、ステップにはピアノの黒鍵が含まれます(0=middle C, 1=C#, 2=D, etc)。
  • MIDIノートの番号付けを使用する場合は、\midinote を使用します(60=middle C, 61=C#, 62=D,etc)。
  • 最後に、ヘルツで直接周波数を指定する場合は、\freq を使用します。

4つの方法すべての比較については、図2を参照してください。

次の例では、4つの Pbind がすべて同じ音符を演奏します:中央のCの上のA (A4)。

Figure02.png 図2:\degree、\note、\midinote、および\freqの比較

Pbind(\degree,5).play;
Pbind(\note,9).play;
Pbind(\midinote,69).play;
Pbind(\freq,440).play;
ヒント:bird2.PNG
ピッチ仕様の各タイプは、異なる合理的な範囲の数値を想定していることに注意してください。[-1,0,1,3] のような数字のリストは、\degree や \note には意味がありますが、\midinote や \freq には意味がありません。次の表では、ピアノキーボードを基準として使用していくつかの値を比較しています。
A0 (lowest piano note) C4 A4 C5 C8 (highest piano note)
\degree -23 0 5 7 21
\note -39 0 9 12 48
\midinote 21 60 69 72 108
\freq 27.5 261.6 440 523.2 4186

13.5 その他のキーワード:振幅とレガート

次の例では、イベントの振幅と音符間のレガートの量を定義する2つの新しいキーワード \amp と \legato を紹介します。良いインデントのおかげでコードが非常に読みやすく、複数行に広がっていることに注目してください。かっこ(上と下)を囲むと、コードブロックを区切ってすばやく実行できます。

(
 Pbind(
    \degree,Pseq([0,-1,2,-3,4,-3,7,11,4,2,0,-3],5),
    \dur,Pseq([0.2,0.1,0.1],inf),
    \amp,Pseq([0.7,0.5,0.3,0.2],inf),
    \legato,0.4
).play;
)

Pbind にはこれらの事前定義されたキーワードの多くがあり、これから詳しく学んでしいきます。ここではそのうちのいくつか、1つはピッチ(\degree、\note、\midinote、または \freq から選択)、1つはデュレーション(\dur)、1つは振幅(\amp)、1つはレガート(\legato)について取り上げます。持続時間はビートです(この場合、1秒あたり1ビート、これがデフォルトです)。振幅は0~1の間でなければなりません(0 =無音、1 =非常に大きい)。そして、レガートは0.1から1の間の値で最適に動作します(レガートが何をするのかわからない場合は、上記の例を0.1、0.2、0.3の順に1まで試し、結果を聞いてください)。

最後の例を出発点として、新しい Pbind を作成します。メロディを変更します。持続時間と振幅の新しいリストを作成します。ピッチに \freq を使用して実験します。必要な場合は、任意のパラメーターに固定数を使用することを常に選択できます。たとえば、メロディ内のすべての音符の長さを0.2秒にしたい場合、Pseq [0.2,0.2,0.2,0.2 ...を書く必要はなく、Pseq([0.2],inf) でもなく、単に全体の Pseq 構造体に0.2を書き込みます。

13.6 Prand

(
Pbind(
    \degree, Prand([2, 3, 4, 5, 6], inf),
    \dur, 0.15,
    \amp, 0.2,
    \legato, 0.1
).play;
)

Prand を Pseq に置き換えて、結果を比較します。次に、持続時間、振幅、レガートにPrand を使用してみてください。

13.7 Pwhite

パターンファミリーのもう1つのポピュラーメンバーは Pwhiteです。これは、等分布乱数ジェネレータです(名前は"ホワイトノイズ"に由来します)。たとえば、Pwhite(100,500) は100から500までの乱数を取得します。

(
Pbind(
    \freq, Pwhite(100, 500),
    \dur, Prand([0.15, 0.25, 0.3], inf),
    \amp, 0.2,
    \legato, 0.3
).trace.play;
)

上記の例は、別の役立つトリックも示しています:再生直前のメッセージトレースです。すべてのイベントに対して選択された値をPostウィンドウに出力します。デバッグや単に何が起こっているかを理解するのに非常に便利です!

Pwhite と Prand の違いに注意してください。どちらもランダム性に関係していても、異なる引数を取り、異なることを行います。Pwhite の括弧内には、Pwhite(low,high) という低い境界と高い境界を指定するだけです。乱数はその範囲内から選択されます。一方、Prand は、項目のリスト(大括弧で囲まれている必要があります)と、多数の繰り返し (Prand([list,of,items],repeats)) を受け取ります。リストからランダムなアイテムが選択されます。

両方で遊んで、違いを完全に理解してください。

ヒント:bird2.PNG
2つの整数を持つ Pwhite は、整数のみを生成します。たとえば、 Pwhite(100,500)は145、568、700などの数値を出力しますが、145.6,450.32 などは出力しません。出力に浮動小数点数が必要な場合は、 Pwhite(100,500.0) と記述します。これは、たとえば、振幅に非常に役立ちます。Pwhite(0,1) を書き込むと、0または1しか得られませんが、 Pwhite(0,1.0) を書くと、すべてが得られます。

次の質問を試して、新しい知識をテストしてください。

​ a) Pwhite(0,10) と Prand([0,4,1,5,9,10,2,3],inf) の出力の違いは何ですか?

​ b) 0から100の間でランダムに選択された整数のストリームが必要な場合、Prand を使用できますか?

​ c) Pwhite(0,3) と Prand([0,1,2,3],inf) の出力の違いは何ですか? Pwhite(0,3.0) と書くとどうなりますか?

​ d) 以下の例を実行します。Cマイナースケール(黒鍵を含む)を演奏するために、\degree ではなく \note を使用します。リスト[0,2,3,5,7,8,11,12]には、ピッチC、D、E、F、G、A、B、Cに対応する8つの数字がありますが、それぞれのイベントの数は例は実際にプレイしますか?どうして?

f:id:honda-satoru:20191104041924p:plain
\note

// Pseq
(
Pbind(
    \note, Pseq([0, 2, 3, 5, 7, 8, 11, 12], 4),
    \dur, 0.15;
).play;
)

// Pseq
(
Pbind(
    \note, Prand([0, 2, 3, 5, 7, 8, 11, 12], 4),
    \dur, 0.15;
).play;
)

// Pwhite
(
Pbind(
    \note, Pseq([0, 2, 3, 5, 7, 8, 11, 12], 4),
    \dur, Pwhite(0.15, 0.5);
).play;
)

回答はこのチュートリアルの最後にあります。2

ヒント:bird2.PNG
Pbind は、最短の内部パターンの再生が終了すると、再生を停止します(各内部パターンのrepeats引数によって決定されます)。

13.8 パターンボキャブラリーの拡張

これまでに、自分で簡単な Pbind を書くことができるはずです。ピッチ、デュレーション、振幅、レガート値を指定する方法と、他のパターン(Pseq,Prand,Pwhite) を埋め込んで興味深いパラメーター変更を生成する方法を知っています。

このセクションでは、パターンの語彙を少し増やします。以下の例では、パターンファミリのさらに6つのメンバーを紹介します。彼らが何をするかを自分で考えてみてください。次の戦略を使用します。

  • 結果のメロディーを聞きます。あなたが聞いたものを記述し分析する。
  • パターン名を見てください:何かを示唆していますか?(例えば、Pshuf は"シャッフル"という言葉を思い出させるかもしれません)。
  • 新しいパターン内の引数(数字)を確認します。
  • 前述の .trace.play を使用して、Post ウィンドウに出力される値を確認します。
  • 最後に、ヘルプファイルを参照して推測を確認します(パターンの名前を選択し、[ctrl + D]を押して対応するヘルプファイルを開きます)。
// パターンの語彙を増やす

// Pser
(
Pbind(
    \note, Pser([0, 2, 3, 5, 7, 8, 11, 12], 11),
    \dur, 0.15;
).play;
)

// Pxrand
// Prand と比較して違いを聞く
(
p = Pbind(
    \note, Pxrand([0, 2, 3, 5, 7, 8, 11, 12], inf),
    \dur, 0.15;
).play;
)

// Pshuf
(
p = Pbind(
    \note, Pshuf([0, 2, 3, 5, 7, 8, 11, 12], 6),
    \dur, 0.15;
).play;
)

// Pslide
// 4つの引数を使用: list, repeats, length, step
(
Pbind(
\note, Pslide([0, 2, 3, 5, 7, 8, 11, 12], 7, 3, 1),
\dur, 0.15;
).play;
)

// Pseries
// 3つの引数を使用: start, step, length
(
Pbind(
    \note, Pseries(0, 2, 15),
    \dur, 0.15;
).play;
)

// Pgeom
// 3つの引数を使用: start, grow, length
(
Pbind(
\note, Pseq([0, 2, 3, 5, 7, 8, 11, 12], inf),
\dur, Pgeom(0.1, 1.1, 25);
).play;
)

// Pn
(
Pbind(
    \note, Pseq([0, Pn(2, 3), 3, Pn(5, 3), 7, Pn(8, 3), 11, 12], 1),
    \dur, 0.15;
).play;
)

これらのパターンの使用を練習します。多くのことができます。Pbindは楽譜のレシピのようなもので、ノートとリズムの固定シーケンスを書くことに限定されないという利点があります。常に変化する音楽パラメーターのプロセスを記述することができます(これは"algorithmic composition"と呼ばれることもあります)。これは、パターンファミリの強力な機能の1つの側面にすぎません。

将来的に、より多くのパターンオブジェクトの必要性を感じた場合、最適な場所は、組み込みのヘルプファイルで利用可能なJames Harkinsの"A Practical Guide to Patterns,"です。*


*オンラインでも http://doc.sccode.org/Tutorials/A-Practical-Guide/PG_01_Introduction.html

14 その他のパターントリック

14.1 コード

Pbinds 内にコードを記述したいですか?それらをリストとして記述します(角括弧で囲まれたコンマ区切り値):

f:id:honda-satoru:20191104041924p:plain
\note

(
Pbind(
    \note,Pseq([[0,3,7],[2,5,8],[3,7,10] ,[ 5,8,12 ]],3),
    \dur,0.15
).play; 
)
//strum 
(
Pbind(
    \note,Pseq([[-7,3,7 ,10],[ 0,3,5,8 ]],2),
    \dur,1,
    \legato,0.4,
    \strum,0.1 //0,0.1,0.2 などを試す
 ).play;
 )

14.2 スケール

ピッチの指定に \degree を使用する場合、キーワード \scale を使用して別の行を追加してスケールを変更できます (注:これは \degree とともにのみ機能し、 \note, \midinote または \freqでは機能しません):

f:id:honda-satoru:20191104041736p:plain
/degree

(
Pbind(
    \scale,Scale .harmonicMinor,
    \degree,Pseq([0,1,2,3,4,5,6,7],1),
    \dur,0.15;
).play;
)
 
// この行を評価して、使用可能なすべてのスケールのリストを表示します。
Scale.directory;
 
// 1度の間に半音が必要な場合は、これを行います:
(
Pbind(
    \degree,Pseq([0,1,2,3,3.1,4],1),
).play;
)
 
// 上記の3.1はスケール`3`の上の半音(この場合、Fの上のF#)を意味します。\scaleを明示的に指定しない場合、Scale.majorが想定されることに注意してください。

14.3 移調

\ctranspose キーワードを使用して、音の移調を実現します。これは、\degree,\note,および \midinote と連動しますが、\freq とは連動しません。 f:id:honda-satoru:20191103111238p:plain

(
Pbind(
    \note,Pser([0,2,3,5,7,8,11,11,12],11),
    \ctranspose,12,//オクターブ上で移調(= 12半音)
    \dur,0.15;
).play;
)

14.4 マイクロトーン

マイクロトーンの書き方:

// \note および \midinote を使用したマイクロトーン:
Pbind(\note,Pseq([0,0.5,1,1.5,1.75,2],1)).play;
Pbind(\midinote,Pseq([60,69,68.5,60.25,70],1)).play;

14.5 テンポ

Pbind の \dur キーに指定する値は拍数です。つまり、1は1拍、0.5は半拍などを意味します。特に指定しない限り、デフォルトのテンポは60 BPM(1分あたりのビート)です。別のテンポで演奏するには、新しいTempoClock を作成するだけです。120ビート/分(BPM)で再生する Pbind は次のとおりです。

(
Pbind(\degree,Pseq([0,0.1,1,2,3,4,5,6,7]),
    \dur,1;
).play(TempoClock(120/60)); // 60秒間で120ビート:120 BPM
)

ところで、上記の Pseq が引数(リスト)を1つしかとっていないことはわかりましたか?常にリストの後に来る繰り返し値はどこにありますか?サンプルがシーケンスを1回だけ再生するのが聞こえますが、なぜですか?これは、すべてのパターン(および実際、SuperCollider の他の多くのオブジェクト)の共通プロパティです。引数を省略すると、組み込みのデフォルト値が使用されます。この場合、Pseq のデフォルトの繰り返しは1です。

Pbind?これは単なる Pbind(\degree,0).play であり、1つのノートを演奏する方法しか知りませんでした。デュレーション、振幅、レガートなどの情報を提供しませんでした。これらの場合、Pbind はデフォルト値を使用し先に進みます。

14.6 休符

休符の書き方です。Rest(0.3)のようにカッコ内の数字は、休符の長さをビートで表したものです。休符は、\dur 行だけでなく、Pbind の任意の場所に移動できます。

(
Pbind(
    \degree,Pwhite(0,10),
    \dur,Pseq([0.1,0.1,0.3,0.6,Rest(0.3),0.25],inf);
).play;
)

14.7 2つ以上の Pbind を一緒に再生

いくつかの Pbind を同時に開始するには、単純にそれらすべてを 1 つのコードブロック内に入れます。

(//大きなブロック
Pbind(
    \freq,Pn(Pseries(110,111,10)),
    \dur,1/2,
    \legato,Pwhite(0.1,1)
).play;
Pbind(
    \freq,Pn(Pseries(220,222,10)),
    \dur,1/4,
    \legato,Pwhite(0.1,1)
).play;
 Pbind(
    \freq,Pn(Pseries(330,333,10)),
    \dur,1/6,
    \legato,0.1
).play;
)//大きなブロックを閉じる

Pbind を時系列に再生するには(単にそれらを手動で次々に評価する以外)、

{} .fork

を使います:

//基本的なフォークの例を使用できます。Postウィンドウを見てください:
(
{
    "one thing".postln;
    2.wait;
    "別のもの".postln;
    1.5.wait;
    "最後の1つ".postln;
}.fork; 
)
//より興味深い例:
(
t = TempoClock(76/60); 
{
Pbind( 
    \note,Pseq([[4,11],[6,9]],32),
    \dur,1/6,
    \amp,Pseq([0.05,0.03],inf)
).play(t);
2.wait;

Pbind(
    \note,Pseq([[-25,-13,-1],[-20,-8,4 ],\rest],3), 
    \dur,Pseq([1,1,Rest( 1)],inf),
    \amp,0.1,
    \legato,Pseq([0.4,0.7,\rest],inf)
).play(t);
2.75.wait;
 
Pbind(
    \note,Pseq([23,21,25,23,21,20,18,16,16,20,21,23,21],inf),
    \dur,Pseq([0.25,0.75,0.25,1.75,0.125,0.125,0.80,0.20,0.125,0.125,1],1),
    \amp,0.1,
    \legato,0.5
).play(t);
}.fork(t);
)

Pbind を同時に、順番に再生する高度な方法については、Ppar と Pspawner を確認してください。fork の詳細については、Routine Help ファイルをご覧ください。

14.8 変数の使用

前のセクション"パターンの語彙を拡張する"で,複数の Pbind に対して同じノートリスト [0,2,3,5,7,8,11,12] を何度も入力する必要があることに気付きましたか?同じものを何度も何度もコピーするのは効率的ではありませんか?プログラミングでは、同じタスクを繰り返し実行していることに気付いたときはいつでも、おそらく同じ目標をよりスマートな戦略を採用して達成する必要があります。

この場合,変数を使用できます。覚えているかもしれませんが、変数を使用すると、データのチャンクを柔軟かつ簡潔に参照できます(必要に応じてセクション12を 確認してください)。以下に例を示します。

// 同じ数字のシーケンスをたくさん使用していますか?変数に保存します:
c = [0,2,3,5,7,8,11,11,12]  
// これで参照できます
Pbind(\note,Pseq(c,1),\dur,0.15).play; 
Pbind(\note,Prand(c,6),\dur,0.15).play;
Pbind(\note,Pslide(c,5,3,1),\dur,0.15).play;

変数の使用を練習する別の例:2つの Pbind を同時にプレイしたいとしましょう。それらの1つは昇順のメジャースケールを実行し、もう1つは1オクターブ上の降順のメジャースケールを実行します。どちらも同じ durations のリストを使用します。これを記述する1つの方法を次に示します。

~scale = [0,1,2,3,4,5,6,7];
~durs = [0.4,0.2,0.2,0.4,0.8,0.2,0.2,0.2];
(
Pbind(
    \degree,Pseq(~scale),
    \dur,Pseq(~durs)
).play; 

Pbind(
    \degree,Pseq(~scale.reverse + 7),
    \dur,Pseq(~durs)
).play;
)

ここで興味深いトリック:変数のおかげで、両方の Pbind に対して同じスケール度と durations のリストを再利用します。2番目のスケールが下降し、 最初のスケールより1オクターブ上になるようにしたかった。

これを実現するには、単にメッセージ .reverse を使用してリストの順序を逆にします (新しい行に"~scale.reverse"と入力し、評価して正確にその内容を確認します)。次に7を追加して、1オクターブ上で移調します(テストして結果を確認します)。*

2つの Pbind を 1つのコードブロックで囲んで同時に再生しました。

演習:上記のコードブロック内に Pbind を1つ追加して、3つの音声が聞こえるようにします。いくつかの異なる方法で両方の変数(~scale および ~durs )を使用します。たとえば、Pseq 以外のパターン内で使用、移調量を変更、durations を逆にしたり乗算したり、 などなど。


*同じ移調を得るために \ctranspose,12 を使用することもできます。

15 Pbind を個別に起動および停止する

これは、Pbind、特に inf を使用して永久に実行されるものに関する非常に一般的な質問です。個々の Pbind を自由に停止および起動するにはどうすればよいですか。答えには変数を使用する必要があり、すぐに完全な例が表示されます。しかし、そこに行く前に、Pbind をプレイしたときに何が起こるかをもう少し理解する必要があります。

15.1 Pbind を楽譜として

Pbind は、一種の楽譜として考えることができます。Pbind は、音を出すためのレシピであり、音楽のパッセージを実現するための一連の指示です。スコアを音楽にするためには、プレーヤーに与える必要があります。それは、スコアを読み、それらの指示に基づいて音を出す人です。これら2つの瞬間を概念的に分けましょう。スコアの定義とそのパフォーマンスです。

//スコアを定義する
(
p = Pbind(
    \midinote,Pseq([57,62,64,65,67,69],inf),
    \dur,1/7
); //ここでは再生しません!
)
 
//プレイするスコアを要求する
p.play;

上記の例の変数 p は単にスコアを保持します。Pbind には閉じ括弧の直後に .play メッセージがないことに注意してください。その時点で音は出ません。2番目の瞬間は、SuperColliderにそのスコアからプレイするように依頼したときです:p.play.

この時点でよくある間違いは、プレーヤーが停止することを期待して p.stop を試すことです。それを試して、それがこのように機能しないことを自分で確認してください。次の段落でその理由を理解できます。

15.2 EventStreamPlayer

[Ctrl] + [Shift] + [P] で post ウィンドウを消去し(実際には必要ありませんが、なぜそうではないのですか?)、p.play を再度評価します。Post ウィンドウを見ると、結果が EventStreamPlayer と呼ばれるものであることがわかります。Pbind で .play を呼び出すたびに、SuperCollider はそのアクションを実現するプレーヤーを作成します。それが EventStreamPlayer です。"今すぐこの楽譜を演奏したい"と言うたびに、目の前でピアニストが具体化するようなものです。いいですね。

ええ、はい。ただし、この匿名の仮想プレーヤーが表示されてジョブを開始した後は、あなたはプレーヤーに話す方法がありません。名前がありません。もう少し技術的に言えば、オブジェクトを作成しましたが、後でそのオブジェクトを参照する方法はありません。たぶん、この時点で、p.stop を実行してもうまくいかない理由がわかるでしょう。プレイヤーと話すのではなく、スコアと話そうとしているようです。

スコア(変数 p に格納されている Pbind )は、開始または停止について何も知りません。これは単なるレシピです。プレーヤーは、開始、停止、"最初から始めてもらえませんか"などを知っている人です。つまり、EventStreamPlayer と話す必要があります。必要なことは、名前を付けること、つまり変数に保存することだけです 。

//これらの行を1つずつ試します
~myPlayer = p.play;
~myPlayer.stop;
~myPlayer.resume;
~myPlayer.stop.reset;
~myPlayer.start;
~myPlayer.stop;

要約すると、Pbind で .play を呼び出すと、EventStreamPlayer が生成されます。EventStreamPlayers を変数に保存すると、後でそれらにアクセスしてパターンを個別に開始および停止できます( [ctrl +.] を使用する必要はありません。すべてを一度に強制終了します)。

15.3 例

このセクションを締めくくるより複雑な例があります。一番上のメロディーはチャイコフスキーの青少年のためのアルバムから借用されており、より低いメロディーが対位法で追加されています。図3は、楽譜のパッセージを示しています。

//スコアを定義する
(
var myDurs = Pseq([Pn(1,5),3,Pn(1,5),3,Pn(1,6),1/2,1/2,1,1,3,1,3] ,inf)* 0.4;
~upperMelody = Pbind(
    \midinote,Pseq([69,74,76,77,79,81,Pseq([81,79,81,82,79,81],2),82,81,79,77,76,74,74 ],inf),
    \dur,myDurs
);
~lowerMelody = Pbind(

    \midinote,Pseq([57,62,61,60,59,58,57,55,53,52,50,49,50,52,50,55,53,52,53,55,57,58,
 61,62,62 ],inf),
    \dur,myDurs
);
)
// 2つを一緒に再生します:
(
~player1 =~upperMelody.play;
~player2 =~lowerMelody.play; 
)
//それらを個別に停止します:
~player1.stop;
~player2.stop;
//その他の利用可能なメッセージ
~player1.resume;
~player1.reset; 
~player1.play;
~player1.start; // playと同じ。

まず、変数の使用に注意してください。その1つである myDurs はローカル変数です。あなたはそれがローカル変数と見分けることができます。なぜなら、それはチルダ(~)で開始されず、先頭で var キーワードで宣言されています。この変数は、両方の Pbind のなかで \dur として使用される全体の Pseq を保持します。myDurs は、スコアを定義する時点でのみ実際に必要となるため、あえてローカル変数を使用することは理にかなっています(ただし、環境変数も問題なく機能します)。この例で見る他の変数は環境変数です。一度宣言されると、それらは SuperCollider パッチのどこでも有効です。

第二に、前述のように、スコアとプレイヤーの分離に注意してください。Pbind が定義されると、すぐには再生されません。閉じ括弧の直後に .play はありません。

Figure03.PNG 図3 pbind Tchaikovsky melodyの対位法:

最初のコードブロックを評価した後、2つの Pbind 定義を変数 ~upperMelody および ~lowerMelody に保存するだけです。この2つは、まだ音を出しません。単なるスコアです。~player1 =~upperMelody.play の行では、上旋律を演奏するジョブを実行するための EventStreamPlayer を作成して、そのプレーヤーには ~player1 と命名します。~player2 についても同じ考えです。これにより、各プレーヤーと会話して、停止、開始、再開などを要求できます。

面倒になる恐れがあるため、最後にもう一度繰り返しましょう。

​ • Pbind は、楽譜のように音を出すための単なるレシピです;

​ • Pbind でメッセージ play を呼び出すと、EventStreamPlayer オブジェクトが作成されます。

​ • この EventStreamPlayer を変数に保存すると、後でアクセスして stop や resume などのコマンドを使用できます。

Part III

言語の詳細

16 オブジェクト、クラス、メッセージ、引数

SuperCollider は、JavaC++ などのオブジェクト指向プログラミング言語です。これが何を意味するのかを説明するのはこのチュートリアルの範囲を超えているので、興味があればウェブ上で検索できるようにします。ここでは、学習しているこの新しい言語をよりよく理解するために知っておく必要があるいくつかの基本的な概念を説明します。

SuperCollider のすべてはオブジェクトです。単純な数字でさえ SC のオブジェクトです。異なるオブジェクトは異なる方法で動作し、異なる種類の情報を保持します。オブジェクトにメッセージを送信することにより、オブジェクトから何らかの情報またはアクションを要求できます。2.squared のようなものを作成すると、メッセージ squared はレシーバーオブジェクト2に送信されます。それらの間のドットは接続を確立します。ところで、メッセージはメソッドとも呼ばれます。

オブジェクトはクラス内で階層的に指定されます。SuperCollider には事前定義されたクラスの膨大なコレクションが付属しており、それぞれに独自のメソッドセットがあります。

これを理解する良い方法があります。Animal というオブジェクトの抽象クラスがあると想像してみましょう。Animal クラスは、すべての動物に共通のいくつかの一般的なメソッド(メッセージ)を定義します。年齢体重写真などの方法を使用して、動物に関する情報を取得できます。移動食事睡眠などの方法は、動物に特定の行動をさせます。それから、Animal の2つのサブクラスを持つことができます。1つは Pet と呼ばれ、もう1つは Wild と呼ばれます。これらのサブクラスのそれぞれには、さらに多くのサブクラスを派生させることができます(Pet から派生した DogCat など)。サブクラスは親クラスからすべてのメソッドを継承し、独自の新しいメソッドを実装して特殊な機能を追加します。たとえば、Dog オブジェクトと Cat オブジェクトの両方が、Animal クラスから継承された .eat メッセージに応答します。Dog.name および Cat.name は、次の名前を返します。

pet: このメソッドは、Pet から派生したすべてのオブジェクトに共通です。Dog には bark メソッドがあるため、Dog.bark を呼び出すと何をすべきかがわかります。Cat.bark はエラーメッセージをスローします(Cat には bark メソッドが用意されていないためです。):

ERROR: Message ’bark’ not understood.

これらすべての仮説例では、大文字で始まる単語はオブジェクトを表すクラスです。ドットの後の小文字の単語は、それらのオブジェクトに送信されるメッセージ(またはメソッド)です。オブジェクトにメッセージを送信すると、常に何らかの情報が返されます。最後に、メッセージは引数を受け入れる(または必要とする)こともあります。引数は、メッセージの直後に括弧で囲まれたものです。Cat.eat( "sardines",2)では、食べるというメッセージと、何を食べるのか、と量という非常に具体的な情報とともに、食べるメッセージが Cat に送信されています。かっこ内で明示的に宣言された引数(コロンで終わるキーワード)が表示される場合があります。これは、引き数が何を参照しているのかをコードを読む人に思い出させるのに便利です。Dog.bark(volume:10) は、単に Dog.bark(10) よりも自明です。

Figure04.PNG 図4 Hypothetical class hierarchy

OK、オブジェクト指向プログラミングのこの迅速で荒っぽい説明はこれで十分です。 SuperCollider で実際に実行できる例をいくつか試してみましょう。 1行ずつ実行し、メッセージ、レシーバーオブジェクト、および引数(存在する場合)を識別できるかどうかを確認します。 基本的な構造は、このドキュメントの最後にある Receiver.message(arguments) Answers です。3

[1, 2, 3, "wow"].reverse;
"hello".dup(4);
3.1415.round(0.1); // 最初のドットは3.1415 4 100.randの10進数のケースであることに注意してください。//この行を数回評価します
// Chaining messages is fun:
100.0.rand.round(0.01).dup(4);

17 レシーバー表記、ファンクション表記

SuperCollider で式を記述する方法は複数あります。上記で見たものはレシーバー表記法と呼ばれます:100.rand 、ここでドットはオブジェクト( 100 )をメッセージ( rand )に接続します。あるいは、まったく同じことを rand( 100 ) のように書くこともできます。これは関数表記法と呼ばれます。

どちらの記述方法でも使用できます。メッセージが2つ以上の引数を取る場合の動作を次に示します。

5.dup(20); // レシーバー表記
dup(5,20); // 関数表記法でも同じこと
3.1415.round(0.1); // レシーバー表記
round(3.1415, 0.1); // 関数表記

上記の例では、dup(5,20) を"数値5を20回複製"と読み、round(3.1415,0.1) を"数値3.1415を1桁の小数に丸める"と読みます。逆に、レシーバー表記バージョンは、“数値5、20回自分自身を複製します!” ( 5.dup(20) の場合 )および"数値3.1415、小数点以下1桁に丸めます!" ( 3.1415.round(0.1) の場合 )として読み取ることができます。つまり、Receiver.message(argument) はmessage(Receiver,argument) と同等です。ある書き方を他の書き方よりも選択することは、個人の好みと慣習の問題です。あるメソッドが他のメソッドよりも明確な場合があります。どんなスタイルを好むように(そしてそれらを混ぜても構いません)、重要なことは一貫性を保つことです。SuperColliderユーザーの間で広まっている慣習の1つは、クラス(大文字で始まる単語)はほぼ常に Receiver.message(argument) として記述されることです。たとえば、SinOsc.ar(440)は常に見られますが、ar(SinOsc,440) は両方とも正しいにもかかわらず、ほとんど見かけることがありません。

演習:関数表記のみを使用して次のステートメントを書き換えます

100.0.rand.round(0.01).dup(4);

回答はこのドキュメントの最後 4

18 ネスティング

最後の演習の解決策により、あるものを別のものの中にネストすることができました。David Cottleは、SuperCollider Bookでネスティングの優れた説明がありますので、ここで引用します。*

ネスティングのアイデアをさらに明確にするために、SCが昼食を作る仮想的な例を考えてみましょう。 そのためには、サーブメッセージを使用します。 引数は、サラダ、メインコース、デザートかもしれません。 ただし、サーブ(レタス、魚、バナナ)と言うだけでは、期待どおりの結果が得られない場合があります。 したがって、安全にするために、これらの引数を明確にし、それぞれをネストされたメッセージと引数に置き換えることができます。

serve(toss(lettuce, tomato, cheese), bake(fish, 400, 20), mix(banana, icecream))

SCは、レタス、魚、バナナだけでなく、レタス、トマト、チーズのサラダを提供します。 焼き魚; バナナサンデー。 これらの内部コマンドは、各成分(レタス、トマト、チーズなど)のmessage(arg)をネストすることでさらに明確にすることができます。 各内部メッセージは結果を生成し、その結果は外部メッセージの引数として使用されます。

// Pseudo-code to make dinner:
serve(
    toss(
        wash(lettuce, water, 10),
        dice(tomato, small),
        sprinkle(choose([blue, feta, gouda]))
    ),
    bake(catch(lagoon, hook, bamboo), 400, 20),
    mix(
        slice(peel(banana), 20),
        cook(mix(milk, sugar, starch), 200, 10)
    )
);

ネストに複数のレベルがある場合、明確にするために新しい行とインデントを使用できます。一部のメッセージと引数は1行に残され、一部は1行に1つの引数を付けて展開されます。各インデントレベルは、ネストのレベルを示す必要があります。(コードのビット間には、任意の量の空白(改行、タブ、またはスペース)を入れることができます。)

[夕食の例]ランチプログラムでは、レタスを水で10分間洗浄し、サラダボウルに投げてチーズを振りかける前に、トマトを細かく切るように指示されています。また、魚を捕まえる場所と、サービングの前に20分間400度で焼く場所などを指定しました。 このスタイルのコードを"読み取る"には、最も内側のネストされたメッセージから開始し、連続する各レイヤーに移動します。 以下は、最も内側のメッセージが外側のメッセージ内にネストされる方法を示すために調整された例です。

            exprand(1.0,1000.0);

        dup({exprand(1.0,1000.0)},100);

    sort(dup({exprand(1.0,1000.0)},100));

round(sort(dup({exprand(1.0,1000.0)},100)),0.01);

*David Cottle “Beginner’s Tutorial.”,SuperCollider Book,MIT Press,2011,pp. 8-9

以下のコードは、ネストの別の例です。 続く質問に答えてください。 数字が何をしているのかを説明する必要はありません。タスクは、ネストの各層で引数を識別することです。 (例と演習の質問も Cottle のチュートリアルから借用され、わずかに修正されています。)

// ネストと適切なインデント
(
{
    CombN.ar(
        SinOsc.ar(
            midicps(
                LFNoise1.ar(3, 24,
                    LFSaw.ar([5, 5.123], 0, 3, 80)
                    )
                ),
                0, 0.4
            ),
            1, 0.3, 2)
}.play;
)

a) LFNoise1.ar の2番目の引数は何番ですか?

b) LFSaw.ar の最初の引数は何ですか?

c) LFNoise1.ar の3番目の引数は何ですか?

d) midicps にはいくつの引数がありますか?

e) SinOsc.ar の3番目の引数は何ですか?

f) CombN.ar の2番目と3番目の引数は何ですか?

回答については、このドキュメントの最後を参照してください。5

ヒント:bird2.PNG
何らかの理由でコードが適切なインデントを失った場合は、そのすべてを選択してメニューから[Edit]→[Autoindent Line or Region(行または領域の自動インデント)]を選択すると修正されます。

19 エンクロージャ

囲いには4つのタイプがあります:( parentheses )、[ brackets ]、{ braces }、および "引用符" 。

開いたものはそれぞれ、後で閉じる必要があります。これは"バランス"と呼ばれます。つまり、コード全体で適切に一致するエンクロージャーのペアを維持します。

SuperCollider IDE は、ペアを閉じると、一致するかっこ (かっことかっこ) を自動的に示します。それらは赤で表示されます。開始/終了の一致がない括弧をクリックすると、何かが足りないことを示す濃い赤色の選択が表示されます。

バランシングは、評価、削除、またはコピー/貼り付け操作のためにコードの大きなセクションを選択する簡単な方法です。開き括弧または閉じ括弧(括弧と括弧)をダブルクリックして、中のすべてを選択できます。

19.1 引用符

引用符は、一連の文字(スペースを含む)を単一の単位として囲むために使用されます。これらは文字列と呼ばれます。一重引用符は、文字列とはわずかに異なるシンボルを作成します。シンボルは、テキストの直前にバックスラッシュを付けて作成することもできます。したがって、'greatSymbol' と\greatSymbol は同等です。

"これは素敵な文字列です"; 

  'greatSymbol';

19.2 括弧

括弧は次の目的で使用できます。

  • 引数リストを囲む:rrand(0,10);

  • 強制の優先順位:5 +(10 * 4);

  • コードブロックを作成します(一緒に評価される複数行のコード)。

19.3 ブラケット

角括弧は、[1,2,3,4, "hello"]のようなアイテムのコレクションを定義します。これらは通常、配列と呼ばれます。配列には、数字、文字列、関数、パターンなど、何でも含めることができます。

配列は、リバース、スクランブル、ミラーリング、選択などのメッセージを理解します。配列に対して数学演算を実行することもできます。

[1,2,3,4, "hello"].scramble;
[1,2,3,4, "hello"].mirror;
[1,2,3,4].reverse + 10;
// midiをHz単位の周波数に変換
[60,62,64,65,67,69,71].midicps.round(0.1);

Arrays の詳細については、セクション22で近日公開予定です。

19.4 Curly Braces

braces(または"curly braces")は関数を定義します。関数は、おそらく複数回使用および再利用される何らかの種類の操作またはタスクをカプセル化し、そのたびに異なる結果を返す可能性があります。以下の例は、SuperCollider bookからのものです。

 exprand(1,1000.0);
 {exprand(1,1000.0)}

David Cottle が彼の例を紹介します: "最初の行はランダムな数字を選択します。これは Post ウィンドウに表示されます。2番目は、まったく異なる結果、つまり関数を出力します。関数は何をしますか? 乱数を選択します。その違いはどのようにコードに影響しますか? 以下の行を考慮してください。最初は乱数を選択して複製します。2番目は乱数抽出関数を5回実行し、結果を配列に収集します。" *

rand(1000.0).dup(5); // 番号を選択して複製します
{rand(1000.0)}.dup(5); // 数字を選ぶ機能を複製します
{rand(1000.0)}.dup(5).round(0.1); // 上記のすべて、そしてラウンド
// 本質的に、これ(同様の結果があります)
[rand(1000.0),rand(1000.0),rand(1000.0),rand(1000.0),rand(1000.0)]

*David Cottle "Beginner’s Tutorial." SuperCollider Book,MIT Press,2011,p.13.

機能についてはもうすぐ。今のところ、すべての可能なエンクロージャーの概要は次のとおりです。

コレクション [list, of, items]

関数 {しばしば複数行のコード}

文字列 "引用符内の単語"

シンボル '単一引用符'または\バックスラッシュが前に付いている

20 条件:if / else および case

雨が降っている場合は、外出するときに傘をもちます。晴れている場合は、サングラスをつけます。私たちの日々は、このような意思決定に満ちています。プログラミングでは、これらはコードが何らかの条件をテストしなければならない瞬間であり、テストの結果(trueまたはfalse)に応じて異なる一連のアクションを実行する必要があります。条件構造には多くの種類があります。if / else と case の2つの単純なものを見てみましょう。

SCの if / else の構文は、if(condition,{true action},{false action}) です。条件はブールテストです(trueまたはfalseを返す必要があります)。テストがtrueを返す場合、最初の関数が評価されます; それ以外の場合、2番目の関数は。試してみてください:

// if / else
if(100> 50,{"非常に真".postln},{"非常に偽".postln});

SuperCollider book *から借用した以下の表は、使用できる一般的なブール演算子を示しています。単一の等号(x = 10)と2つの等号(x == 10)の違いに注意してください。シングルサインは"変数xに10を割り当てる"ことを意味し、ダブルサインは"xが10に等しいか"を意味します。true または false カラムからいくつかの例を入力して実行すると、実際に true または false の結果が表示されます Post ウィンドウ。

Symbol Meaning True Example False Example
== equal to? 10 == 10 10 == 99
!= not equal to? 10 != 99 10 != 10
> greater than? 10 > 5 10 > 99
< less than? 10 < 99 10 < 5
>= greater than or equal to? 10 >= 10, 10 >= 3 10 >= 99
<= less than or equal to? 10 <= 99, 10 <= 10 10 <= 9
odd is it odd? 15.odd 16.odd
even is it even? 22.even 21.even
isInteger is it an integer? 3.isInteger 3.1415.isInteger
isFloat is it a float? 3.1415.isFloat 3.isFloat
and both conditions 11.odd.and(12.even) 11.odd.and(13.even)
or either condition or(1.odd, 1.even) or(2.odd, 1.even)

最後の2行( and, or )は、レシーバー表記または関数表記のいずれかで長い式を記述する方法を示しています。

別の有用な構造は、case です。テストの1つが true を返すまで順番に評価される関数のペアを定義することで機能します。


*Cottle, D. “Beginner’s Tutorial.” The SuperCollider Book, MIT Press, 2011, p. 33

Case

{test1} {action1}
{test2} {action2}
{test3} {action3}
. . .
{testN} {actionN};

各テスト内の式は、true または false を返す必要があります。 test1 が false を返す場合、プログラムは action1 を無視し、test2 に進みます。 false の場合、action2 も無視され、test3 に進みます。 それが真であることが判明した場合、action3 が実行され、ケースは停止します(それ以上のテストまたはアクションは実行されません)。 関数間にコンマがないことに注意してください。 case ステートメントの終わりを示すために、最後にセミコロン ; を使用するだけです。

// case
(
~num = -2;

case
{~num == 0} {"WOW".postln}
{~num == 1} {"ONE!".postln}
{~num < 0} {"negative number!".postln}
{true} {"last case scenario".postln};
)

上記のコードを変更して、考えられるすべての結果を取得してください。 上記の例のケースの最後の行にある便利な(およびオプションの)トリックに注意してください。true は常に true に評価されるため、前の条件がすべて falseになった場合に常に発生する"最後のケースシナリオ"アクションを定義できます。 詳細については、制御構造のヘルプファイルをご覧ください。

21 関数

同じタスクを数回実行していることに気付いたら、再利用可能な関数を作成するのがよいかもしれません。関数は、"エンクロージャー"セクションで学習したように、中括弧内に記述されたものです。David Touretzky は、次のように関数の概念を紹介しています。"関数は、データが流れるボックスとして考えてください。関数は何らかの方法でデータを操作し、その結果が流出します。"*

Figure05.PNG 図5:関数の一般的な考え方。

以下の例の最初の行は、関数を定義し、それを変数fに割り当てます。2行目は、関数を機能させます。

f = { 2 + 2 }; // 関数を定義します
f.value; // 関数を機能させる

上記の関数は、1つのこと(2と2を追加)を行う方法しかわからないため、それほど便利ではありません。 通常は、入力引数に応じて異なる結果が得られる関数を定義します。 キーワード arg を使用して、関数が受け入れることができる入力を指定します。

以下の例は、図5の図面に似ています。

f = {arg a, b; ["a plus b", a+b, "a times b", a*b].postln}; // 関数を定義
f.value(3, 7); // これで、関数の引数として任意の2つの数値を指定できます
f.value(10, 14);

// 比較:
~sillyRand = rrand(0, 10); // 関数ではありません
~sillyRand.value; // 複数回評価
~sillyRand2 = {rrand(0, 10)}; // 関数
~sillyRand2.value; // 数回評価する

*Touretzky, David. COMMON LISP: A Gentle Introduction to Symbolic Computation. The Benjamin/Cummings Publishing Company, Inc, 1990, p. 1. このチュートリアルのタイトルに影響を与えた本です。

最後の例として、非常に便利な関数を1つ示します。

//この関数を使用して、夏の日を過ごす方法を決定します
(
~whatToDo = {
var today, dayName, actions;
    today = Date.getDate.dayOfWeek;
    dayName =
    case
    {today==0} {"Sunday"}
    {today==1} {"Monday"}
    {today==2} {"Tuesday"}
    {today==3} {"Wednesday"}
    {today==4} {"Thursday"}
    {today==5} {"Friday"}
    {today==6} {"Saturday"};
    actions = ["boomerang throwing", "arm wrestling", "stair climbing", "playing
    chess", "underwater hockey", "pea shooting", "a nap marathon"];
    "Ah, " ++ dayName ++ "...! " ++ "What a good day for " ++ actions.choose;
};
)

// 朝に実行する
~whatToDo.value;
ヒント:bird2.PNG

​ 関数の先頭で引数を宣言する別の一般的な表記法は次のとおりです。

f={| a,b | a + b} 

​ これは、

f = {arg a, b; a + b}

​ と同等です。

22 配列の楽しみ

配列は、SuperCollider で最も一般的なタイプのコレクションです。[0,1,2]のように、角かっこで囲まれたアイテムのコレクションを記述するたびに、それは Array クラスのインスタンスになります。多くの場合、さまざまな方法で配列を操作します。配列が理解できる興味深いメソッドの一部を以下に示します。

// 配列を作成する
a = [10, 11, 12, 13, 14, 15, 16, 17];

a.reverse; // リバース
a.scramble; // スクランブル
a.choose; // ランダムに1つの要素を選択します
a.size; // 配列のサイズを返します
a.at(0); // 指定された位置のアイテムを取得します
a[0] ; // 同上
a.wrapAt(9); // 指定された位置にあるアイテムを取得し、> a.sizeの場合は折り返します
["wow", 99] ++ a; // 2つの配列を新しいものに連結します
a ++ \hi; // シンボルは単一の文字です
a ++ 'hi'; // シンボルは単一の文字です
a ++ "hi"; // 文字列は文字のコレクションです
a.add(44); // 最後に新しい要素を持つ新しい配列を作成します
a.insert(5, "wow"); // 位置5に"wow"を挿入し、他のアイテムを前方にプッシュします(新しい配列を返します)
a; // これを評価し、上記の操作のどれも実際に元の配列を変更していないことを確認します
a.put(2, "oops"); // インデックス2に "oops"を置きます(destructive;上の行をもう一度評価して確認します)
to check)
a.permute(3); // 位置3のアイテムは位置0に移動し、逆も同様です
a.mirror; // それを回文にします
a.powerset; // 配列の要素のすべての可能な組み合わせを返します

配列を使用して数学を行うことができます:

[1, 2, 3, 4, 5] + 10;
[1, 2, 3, 4, 5] * 10;
([1, 2, 3, 4, 5] / 7).round(0.01); // 優先順位の括弧に注意してください
x = 11; y = 12; // いくつかの変数を試します
[x, y, 9] * 100;
// ただし、適切な数値でのみ数学を行うようにしてください
[1, 2, 3, 4, "oops", 11] + 10; // 奇妙な結果

22.1 新しい配列の作成

クラス Array を使用して新しいコレクションを作成するいくつかの方法を次に示します。

// 算術級数 Arithmetic series
Array.series(size: 6, start: 10, step: 3);
// 幾何級数 Geometric series
Array.geom(size: 10, start: 1, grow: 2);
// 2つを比較する:
Array.series(7, 100, -10); // 7アイテム; 100から開始、-10のステップ
Array.geom(7, 100, 0.9); // 7アイテム; 100から開始。毎回0.9倍する
// .fillメソッドを満たす
Array.fill(10, "same");
// 比較:
Array.fill(10, rrand(1, 10));
Array.fill(10, {rrand(1, 10)}); // 関数は10回再評価されます
// .fillメソッドの関数は、カウンターであるデフォルトの引数を取ることができます。
// 引数名は何でも構いません。
Array.fill(10, {arg counter; counter * 10});
// たとえば、高調波周波数のリストを生成します:
Array.fill(10, {arg wow; wow+1 * 440});
// .newClearメソッド The .newClear method
a = Array.newClear(7); // 指定されたサイズの空の配列を作成します
a[3] = "wow"; // a.put(3, "wow")と同じ

22.2 その面白い感嘆符

他の人のコードに30!4のようなものが表示されるのは時間の問題です。このショートカット表記は、同じアイテムを含む配列を何度も作成するだけです。

// ショートカット表記 :
30!4;
"hello" ! 10;
// 以下と同じ結果が得られます:
30.dup(4);
"hello".dup(10);
// または
Array.fill(4, 30);
Array.fill(10, "hello");

22.3 括弧の間の2つの点

配列の作成に使用されるもう1つの一般的な構文のショートカットを次に示します。

// これは?
(50..79);

// 算術級数の配列を生成するショートカットです
// 上記の結果は次と同じです:
series(50, 51, 79);
// または
Array.series(30, 50, 1);
// 1以外のステップの場合、これを行うことができます:
(50, 53 .. 79); // step of 3
// 同じ結果:
series(50, 53, 79);
Array.series(10, 50, 3);

各コマンドは、わずかに異なる考え方を暗示していることに注意してください。(50..79)を使用すると、"50~79の配列を指定してください"というように考えることができます。配列に含まれるアイテムの数を必ずしも考慮する必要はありません。一方、Array.seriesを使用すると、"合計で30個のアイテムを含む配列を50個からカウントアップしてください"と考えることができます。シリーズの最後の番号が誰になるかを必ずしも考える必要はありません。

また、ショートカットでは角括弧ではなく括弧が使用されることに注意してください。もちろん、結果の配列は角括弧で囲まれます。

22.4 配列を"do"する方法

多くの場合、コレクションのすべてのアイテムに対して何らかのアクションを実行する必要があります。これにはメソッドdoを使用できます。

~myFreqs = Array.fill(10, {rrand(440, 880)});

// ここで、リストのすべての項目で簡単なアクションを実行しましょう:
~myFreqs.do({arg item, count; ("Item " ++ count ++ " is " ++ item ++ " Hz. Closest
midinote is " ++ item.cpsmidi.round).postln});

// カウンターが必要ない場合は、1つの引数を使用します:
~myFreqs.do({arg item; {SinOsc.ar(item, 0, 0.1)}.play});
~myFreqs.do({arg item; item.squared.postln});

// もちろん、次のように最後の1つと同じくらい簡単なことができます:
~myFreqs.squared;

要約すると、配列を"do"すると、関数が提供されます。メッセージdoは、配列のアイテムを繰り返し処理し、その関数を毎回評価します。この関数は、デフォルトで2つの引数を取ることができます。現在の反復での配列項目と、反復回数を追跡するカウンターです。これらの引数の名前は好きなものにすることができますが、それらは常にこの順序です: item, count.

メソッド collect も参照してください。これは、do と非常に似ていますが、すべての中間結果を含む新しいコレクションを返します。

23 困ったときは

ヘルプファイルを活用する方法を学びます。多くの場合、各ヘルプページの下部に役立つ例があります。最初にテキストの説明を完全に理解していない場合でも、(または特別に)下にスクロールしてチェックアウトしてください。ヘルプブラウザーからサンプルを直接実行するか、コードをコピーして新しいウィンドウに貼り付けて再生することができます。

SuperCollider コードで有効なクラスまたはメソッドを選択し(単語をダブルクリックして選択します)、[ctrl + D]を押して対応するヘルプファイルを開きます。クラス名(たとえば、MouseX)を選択すると、クラスのヘルプファイルが表示されます。メソッドを選択すると、そのメソッドを理解するクラスのリストが表示されます(たとえば、メソッド scramble のヘルプを表示させます)。*


*注意:SuperColliderは大文字で始まる単語を青色で表示します。この、青色 は、単語にタイプミスがないことを保証するものではありません。たとえば、Sinosc (小文字の "o"を間違えた) を入力すると、青色で表示されます。

SuperCollider IDEでヘルプファイルを探索する他の方法は、"Browse" および "Search" リンクです。[Browse]を使用してファイルをカテゴリ別に移動し、[Search] を使用してすべてのヘルプファイル内の単語を検索します。SuperCollider IDEのヘルプブラウザに関する重要な注意:

  • 右上のフィールド(“Find...”と表示)を使用して、現在開いているヘルプファイル内の特定の単語を検索します(Webサイトで"検索"を実行する場合など)。

  • [Search]リンク(“Browse”の右側)を使用して、すべてのヘルプファイルでテキストを検索します。

最初に括弧を開いて特定のメソッドに引数を追加すると、SCは小さな"ツールヒントヘルプ"を表示して、予想される引数が何であるかを示します。たとえば、図6に表示される行の先頭を入力します。最初のかっこを開いた直後に、SinOsc.arの引数がfreq,phase,mul, および add であることを示すツールチップが表示されます。また、デフォルト値が何であるかを示しています。これは、SinOscヘルプファイルから取得する情報とまったく同じです。ツールチップが消えた場合は、[ctrl + Shift + Space] で元に戻すことができます。

Figure06.PNG 図6:入力すると役立つ情報が表示されます

別のショートカット:引数に明示的に名前を付けたい場合 ( SinOsc.ar(freq:890) など )、括弧を開いた直後にタブキーを押してみてください。SCは入力時に正しい引数名を順番に入力します(後続の引数名のコンマの後にタブを押します)。

ヒント:bird2.PNG
独自の"パーソナライズされたヘルプファイル"を含むフォルダーを作成します。新しいトリックを見つけたり、新しいオブジェクトを学習したりするときは、自分の言葉で説明した簡単な例を書き、将来のために保存します。今から1か月または1年後に便利になるかもしれません。

まったく同じヘルプファイルは、オンラインで見つけることもできます。http://doc.sccode.org/

Part IV

音声合成と処理

この時点で、SuperColliderについてすでに多くのことを知っています。このチュートリアルの最後の部分では、変数からエンクロージャーまで、言語自体についての詳細を紹介しました。また、パターンファミリのいくつかのメンバーを使用して、興味深いPbind を作成する方法も学びました。

チュートリアルのこの部分では、(最終的には!)SuperColliderを使用したサウンドの合成と処理について説明します。ユニットジェネレーター( UGens )のトピックから始めます。*

24 UGens

セクション3およびセクション18で、いくつかのユニットジェネレーター( UGens )が動作しているのを既に見てきました。ユニットジェネレーターは、音声信号または制御信号を生成するオブジェクトです。 これらの信号は常にサーバーで計算されます。 ユニットジェネレータには多くのクラスがあり、それらはすべて UGen クラスから派生しています。 SinOsc と LFNoise0 は UGen の例です。 詳細については、"Unit Generators and Synths" および "Tour of UGens" というヘルプファイルをご覧ください。

このチュートリアルの前半で Pbinds をプレイしたとき、デフォルトのサウンドは常に同じで、シンプルなピアノのようなシンセです。 そのシンセはユニットジェネレーターの組み合わせで作られています。 †学習します。


*ほとんどのチュートリアルは、すぐにユニットジェネレーターから始まります。ただし、SCのこのイントロでは、異なる教育学的アプローチのために、最初にパターンファミリー( Pbind と友人)を強調することを選択しました。 †これまでは、Pbinds を使用してSuperColliderサウンドを作成していたため、"そうだ、Pbind はユニットジェネレーターだ!"とは思わないかもしれません。 Pbind はユニットジェネレータではなく、音楽イベント(スコア)を作成するための単なるレシピです。 "だから、EventStreamPlayer、Pbind で play を呼び出したときに生じるもの、それは UGen でなければならない!" という答えはまだノーです。 EventStreamPlayer は、ピアニストのような単なるプレーヤーであり、ユニットジェネレータを組み合わせて、あらゆる種類の電子楽器を合成音と処理音で作成する方法です。 次の例は、最初の正弦波から作成して、マウスでライブ演奏できる電子楽器を作成します。

24.1 マウス制御:インスタントテルミン

ライブで演奏できるシンプルなシンセをご紹介します。これは、最も古い電子楽器の1つであるテルミンのシミュレーションです。

{SinOsc.ar(freq: MouseX.kr(300, 2500), mul: MouseY.kr(0, 1))}.play;

テルミンとは何かわからない場合は、今すぐすべてを停止し、YouTubeで“Clara Rockmore Theremin”を検索してください。その後、ここに戻って、SCテルミン白鳥の歌を演奏してみてください。

SinOsc、MouseX、および MouseY は UGen です。SinOsc は正弦波トーンを生成しています。 他の2つは、画面上のカーソルの動き(水平方向の動きはX、垂直方向の動きはY)をキャプチャし、数値を使用して周波数と振幅値を正弦波に送ります。 非常にシンプルで、とても楽しいです。

24.2 のこぎりとパルス。プロットとスコープ。

上記のテルミンは正弦波発振器を使用しました。サウンドを作成するために使用できる他の波形があります。以下の行を実行します(便利なプロット方法を使用します) SinOsc の形を見て、Saw と Pulse と比較します。以下の行は音を出しません。波形のスナップショットを視覚化するだけです。


ピアニストは音を出しません。この限られた metaphor に沿って、楽器のピアノは実際に振動して音を生成します。これは UGen のより適切な例えです。スコアでもプレイヤーでもありません。楽器です。以前に Pbinds で音楽を作成した場合、SCは EventStreamPlayer を作成して、組み込みのピアノシンセでスコアを再生します。ピアノの作成などを心配する必要はありませんでした。SuperColliderはすべての作業をボンネットの下で行ってくれました。その隠されたピアノシンセは、いくつかのユニットジェネレーターの組み合わせで作られています。

{ SinOsc.ar }.plot; // 正弦波 sine wave
{ Saw.ar }.plot; // のこぎり波 sawtooth wave
{ Pulse.ar }.plot; // 方形波 square wave

次に、SinOsc を Saw に、次に Pulse に置き換えて、テルミンの行を書き換えます。音の違いを聞いてください。最後に、テルミンコードで .play の代わりに .scope を試すと、リアルタイムで波形の表現を見ることができます("Stethoscope"ウィンドウがポップアップ表示されます)。

25 音声レート、制御レート

SuperCollider コードで UGen を見つけるのは非常に簡単です。ほとんどの場合、メッセージの後に .ar または .kr のメッセージが続きます。これらの文字は、オーディオレートとコントロールレートを表しています。これが何を意味するのか見てみましょう。

"Unit Generators and Synths" ヘルプファイルから:

ユニットジェネレーターは、arまたはkrメッセージをユニットジェネレーターのクラスオブジェクトに送信することによって作成されます。arメッセージは、オーディオレートで実行されるユニットジェネレーターを作成します。krメッセージは、制御レートで実行されるユニットジェネレーターを作成します。制御レートユニットジェネレーターは、低周波数またはゆっくり変化する制御信号に使用されます。コントロール レートユニット ジェネレーターは、制御サイクルごとに1つのサンプルのみを生成するため、オーディオレートユニットジェネレーターよりも処理能力が低くなります。*

つまり、SinOsc.ar を作成すると、"オーディオレート"というメッセージが SinOsc UGen に送信されます。コンピューターが44100 Hzの一般的なサンプリングレートで実行されていると仮定すると、この正弦発振器は、1秒あたり44100サンプルを生成して、スピーカーに送信します。 次に、正弦波が聞こえます。

今読んだものについてもう一度考えてみてください。ar メッセージを UGen に送信すると、毎秒4万4千個の数字を生成するように指示しています。 それはたくさんの数字です。{SinOsc.ar}.play を言語で記述し、言語がリクエストをサーバーに伝えます。 これらすべてのサンプルを生成する実際の作業は、サーバー、SuperColliderの"サウンドエンジン"によって行われます。


*http://doc.sccode.org/Guides/UGens-and-Synths.html

現在、 ar の代わりに kr を使用すると、ジョブもサーバーによって実行されますが、いくつかの違いがあります。

  1. .krで生成される1秒あたりの数値の量ははるかに少なくなります。{SinOsc.ar} .play は毎秒44100の数値を生成しますが、{SinOsc.kr} .play は毎秒700未満の数値を出力します(興味がある場合、正確な量は44100/64です。64はいわゆる"control period."。)

  2. krで生成された信号はスピーカーに送られません。代わりに、通常、他の信号のパラメーターを制御するために使用されます。たとえば、テルミンの MouseX.kr が SinOsc の周波数を制御していました。

OK、それで UGen はこれらの非常に高速な数値のジェネレータです。これらの数値の一部は音声信号になります。その他は制御信号になります。ここまでは順調ですね。しかし、結局のところ、これらの数字は何ですか? 大きい? 小さい? ポジティブ? 負? 多くの場合、-1から+1の間、時には0から1の間の非常に小さい数値であることがわかります。すべての UGen は、生成する数値の範囲に従って、ユニポーラ UGen とバイポーラ UGen の2つのカテゴリに分類できます。

ユニポーラ UGen は、0~1の数値を生成します。

バイポーラ UGen は、-1~+1の数値を生成します。

25.1 pollメソッド

一部の UGen の出力を覗き見ると、これがより明確になります。SuperCollider が Post ウィンドウに毎秒数千の数字を印刷することは期待できませんが、毎秒数個を印刷するように依頼することはできます。次の行を一度に1つずつ入力して実行し(サーバーが実行されていることを確認してください)、Post ウィンドウを監視します。

// post ウィンドウを見るだけ(音なし)
{SinOsc.kr(1).poll}.play;
// ctrl + ピリオドを押して、次の行を評価します:
{LFPulse.kr(1).poll}.play;

例では kr を使用しているため音が出ません。結果は制御信号であるため、スピーカーには何も送信されません。ここでのポイントは、SinOsc の典型的な出力を見るだけです。メッセージポーリングは、SinOsc 出力から1秒あたり10個の数字を取得し、それらを Post ウィンドウに出力します。引数1は周波数で、サインを意味します。

サイクル全体を完了するには1秒かかります。あなたが観察したことに基づいて、SinOsc は単極ですか、双極ですか? LFPulse はどうですか?6

次の行を評価する前にボリュームを下げてから、ゆっくりと戻します。 柔らかいクリック音が聞こえるはずです。

{LFNoise0.ar(1).poll}.play;

メッセージ ar を送信したため、この低周波ノイズジェネレーターは、サウンドカードに毎秒44100サンプルを出力しています。これはオーディオ信号です。各サンプルは-1~+1の数値です(したがって、バイポーラ UGen です)。投票では、1秒あたり10件しか表示されません。LFNoise0.ar(1) は、毎秒新しい乱数を選択します。これらはすべてサーバーによって実行されます。[ctrl +.]でクリックを停止し、LFNoise0 の周波数を変更してみてください。3、5、10などの数字を試してください。出力番号を見て、結果を聞きます。

26 UGen 引数

ほとんどの場合、使用している UGen の引数を指定する必要があります。あなたはすでにそれを見ました:{SinOsc.ar(440)}.play を書くとき、数字440は SinOsc.ar の引数です。聞こえる周波数を指定します。{SinOsc.ar(freq:440、mul:0.5)}.play のように、引数の名前を明示することができます。引数名は freq および mul です(コード内の単語の直後のコロン:に注意してください)。mul は"乗数"の略で、基本的には波形の振幅です。mul を指定しない場合、SuperCollider はデフォルト値の1(最大振幅)を使用します。mul の使用:0.5は、波形を半分に乗算することを意味します。つまり、最大振幅の半分で再生します。テルミンコードでは、SinOsc の引数 freq および mul に明示的に名前が付けられています。MouseX.kr(300、2500) は、テルミンの周波数をコントロールするために使用されました。MouseX.kr は、出力範囲の下限と上限の2つの引数を取ります。それが、数字300と2500です。振幅を制御する MouseY.kr(0、1) についても同じです。マウス UGen 内の引数には明示的な名前は付けられませんでしたが、そうすることはできます。

UGen がどのような引数を受け入れるかをどのようにして見つけますか?対応するヘルプファイルに移動します:UGen 名をダブルクリックして選択し、[ctrl + D]を押してドキュメントページを開きます。たとえば、MouseX でこれを行います。[ Description ]セクションの後に、[クラスメソッド]セクションが表示されます。そこには、kr メソッドの引数が minval、maxval、warp、および lag であると書かれています。同じページから、それぞれの機能を確認できます。

引数を指定しない場合、SCはヘルプファイルに表示されるデフォルト値を使用します。引数に明示的に名前を付けない場合は、ヘルプファイルに示されている正確な順序で引数を指定する必要があります。明示的に名前を付けた場合は、任意の順序で並べることができ、途中でスキップすることもできます。引数を明示的に命名することも、コードをよりよく理解するのに役立つため、優れた学習ツールです。以下に例を示します。

// minvalとmaxvalが順番に提供され、キーワードなし
{MouseX.kr(300, 2500).poll}.play;
// minval,maxval,およびlagが提供され、ワープをスキップ
{MouseX.kr(minval: 300, maxval: 2500, lag: 10).poll}.play;

27 スケーリング範囲

実際の楽しみは、いくつかの UGen を使用して他の UGen のパラメーターをコントロールするときに始まります。テルミンの例はまさにそれを行いました。:今、あなたはセクション3の例のひとつを正確に理解するためのすべてのツールを持っています。例の中で最後の3行はステップバイステップで、どのようにして LFNoise0 で周波数をコントロールするか方法を示します。

{SinOsc.ar(freq: LFNoise0.kr(10).range(500, 1500), mul: 0.1)}.play;

// 分解する :
{LFNoise0.kr(1).poll}.play; // シンプルなLFNoise0の動作を見ます
{LFNoise0.kr(1).range(500, 1500).poll}.play; // .rangeがあります
{LFNoise0.kr(10).range(500, 1500).poll}.play; // now faster

27.1 スケールとrangeメソッド

range メソッドは、単に UGen の出力を再スケーリングします。LFNoise0 は -1 から +1 までの数を生成することを覚えておいてください(バイポーラ UGenです)。これらの生の数値は、周波数を制御するのにあまり有用ではありません(人間の聴力範囲内で適切な数値が必要です)。.range は -1 と +1 の間の出力を受け取り、引数として指定した低い値と高い値(この場合は500と1500)に合わせてスケーリングします。LFNoise0.kr の引数である数値10は、UGen の頻度、つまり1秒あたり何回新しい乱数を選択するかを指定します。

要するに、UGen を使用して別の UGen のパラメーターを制御するには、まず、必要な数値の範囲を知る必要があります。数値は周波数になりますか?たとえば、100~1000 の範囲でそれらを使用しますか?それとも振幅ですか?おそらく、振幅を0.1(ソフト)から0.5(最大の半分)にしたいでしょうか?または、高調波の数を制御しようとしていますか? 5から19の間にしたいですか?

必要な範囲がわかったら、.range メソッドを使用して、コントロールする UGen に正しい動作をさせます。

演習:周波数が LFPulse.kr によってコントロールされる正弦波を再生する単純なラインコードを記述します(適切な引数を提供します)。次に、.range メソッドを使用して、LFPulse の出力を聞きたいものにスケーリングします。

27.2 MULを使用したスケーリングと追加

これで、.range メソッドを使用して、サーバーで UGen の出力をスケーリングする方法がわかりました。ほぼすべての UGen が持っている引数 mul および add を使用することにより、同じことをより基本的なレベルで実現できます。以下のコードは、バイポーラ UGen とユニポーラ UGen の両方で、範囲アプローチと mul / add アプローチの等価性を示しています。

// こちらは:
{SinOsc.kr(1).range(100, 200).poll}.play;
// ...これと同じ:
{SinOsc.kr(1, mul: 50, add: 150).poll}.play;

// こちらは:
{LFPulse.kr(1).range(100, 200).poll}.play;
// ...これと同じ:
{LFPulse.kr(1, mul: 50, add: 100).poll}.play;

図7は、UGen 出力の再スケーリングにおける mul および add の動作を視覚化するのに役立ちます(SinOsc がデモとして使用されます)。

Figure07.PNG 図7: mul と add による UGen スケーリングレンジ

27.3 linlin and friends

その他の任意のスケーリングには、便利なメソッド linlin、linexp、explin、expexp を使用できます。メソッド名は、それらが何をするかのヒントを示します。線形範囲を別の線形範囲に変換( linlin )、線形から指数に変換( linexp )など。

// たくさんの数字
a = [1, 2, 3, 4, 5, 6, 7];
// 0から127に再スケーリング、線形から線形
a.linlin(1, 7, 0, 127).round(1);
// 0から127、線形から指数
a.linexp(1, 7, 0.01, 127).round(1); // 指数範囲にゼロを使用しないでください

線形および指数関数のレビューについては、算術シーケンスと幾何学的シーケンスの違いをオンラインで調べてください。簡単に言えば、線形(算術)シーケンスは「1、2、3、4、5、6」または「3、6、9、12、15」などです。指数(幾何)シーケンスは「1、2、4、8、16、32」または「3、9、27、81、243」などのようになります。

28 個々のシンセを停止する

これは、いくつかのシンセを開始し、それらを個別に停止できる非常に一般的な方法です。例は自明です:

// 一度に1行ずつ実行します(間に音を止めないでください):
a = { Saw.ar(LFNoise2.kr(8).range(1000, 2000), mul: 0.2) }.play;
b = { Saw.ar(LFNoise2.kr(7).range(100, 1000), mul: 0.2) }.play;
c = { Saw.ar(LFNoise0.kr(15).range(2000, 3000), mul: 0.1) }.play;
// シンセを個別に停止する:
a.free;
b.free;
c.free;

29 設定メッセージ

他の関数(レビューセクション21)と同様に、シンセ関数の先頭で指定された引数にはユーザーがアクセスできます。これにより、シンセのパラメーターをオンザフライで変更できます(シンセの実行中)。そのためにメッセージセットが使用されます。簡単な例:

x = {arg freq = 440, amp = 0.1; SinOsc.ar(freq, 0, amp)}.play;
x.set(\freq, 778);
x.set(\amp, 0.5);
x.set(\freq, 920, \amp, 0.2);
x.free;

デフォルト値(上記の440や0.1など)を指定することをお勧めします。そうしないと、"空の"パラメーターに適切な値を設定するまでシンセが再生されません。

30 オーディオ bus

オーディオ bus は、オーディオ信号のルーティングに使用されます。それらはミキシングボードのチャンネルのようなものです。SuperCollider にはデフォルトで128個のオーディオ bus があります。コントロール busもあります(コントロール信号用)が、ここではオーディオ bus のみに注目しましょう。*

[ctrl + M]を押して、メーターウィンドウを開きます。すべての入力および出力のレベルが表示されます。図8は、このウィンドウのスクリーンショットSuperColliderのデフォルトバスへの対応を示しています。SuperCollider では、オーディオバスには0〜127の番号が付けられます。最初の8つ(0〜7)は、デフォルトでサウンドカードの出力チャンネルとして予約されています。次の8つ(8〜15)は、サウンドカードの入力用に予約されています。他のすべて(16〜127)は、オーディオ信号をある UGen から別の UGen にルーティングする必要がある場合など、任意の方法で自由に使用できます。


*セクション41でコント―ロール bus を簡単に見ていきます。

30.1 Out and In UGens

次のコードを試してください:

{Out.ar(1, SinOsc.ar(440, 0, 0.1))}.play; // 右チャンネル

Figure08.png 図8:SCのオーディオ bus とメーターウィンドウ

Out UGen は、特定の bus への信号のルーティングを処理します。 Out の最初の引数は、ターゲット bus、つまり、この信号の送信先です。上記の例では、番号1は、サウンドカードの正しいチャネルである bus 1に信号を送信することを意味します。 Out.ar の2番目の引数は、そのバスに「書き込む」実際の信号です。 単一の UGen、または UGen の組み合わせにすることができます。例では、これは単なる正弦波です。右のスピーカー(またはヘッドフォンを使用している場合は右の耳)でのみ聞こえます。 メーターウィンドウを開いて表示した状態で、Out.ar の最初の引数を変更します。 0から7までの数字を試して、メーターを見てください。指示した場所に信号が送られることがわかります。

ヒント:bird2.PNG
ほとんどの場合、2つのチャンネル(左右)しか再生できないサウンドカードがあるため、サイントーンは bus 0 または bus 1 に送信したときにのみ聞こえます。他の bus に送信すると(3〜 7)、対応するメーターに信号が表示されます:SCは実際にその bus にサウンドを送信していますが、8チャンネルのサウンドカードがない限り、bus 3〜7の出力を聞くことはできません。

エフェクトに使用されるオーディオ bus の簡単な例を以下に示します。

// start the effect
f = {Out.ar(0, BPF.ar(in: In.ar(55), freq: MouseY.kr(1000, 5000), rq: 0.1))}.play;
// start the source
n = {Out.ar(55, WhiteNoise.ar(0.5))}.play;

最初の行は、フィルター UGen(バンドパスフィルター)で構成されるシンセ(変数 fに格納)を宣言します。バンドパスフィルターは、入力として任意のサウンドを受け取り、通過させたい1つの周波数領域を除くすべての周波数をフィルター処理します。In.ar は、オーディオバスからの読み取りに使用する UGen です。したがって、BPF の入力として In.ar(55) が使用されると、バス55に送信するサウンドはすべてバンドパスフィルターに渡されます。 この最初のシンセは最初は何も音を立てないことに注意してください: 最初のラインを評価しても、バス55はまだ空です。bus 55 にオーディオを送信するときにのみ音がします。これは2行目で発生します。

2行目はシンセを作成し、変数 n に保存します。このシンセは、単にホワイトノイズを生成し、スピーカーに直接出力するのではなく、代わりにオーディオ bus 55に出力します。これがまさにフィルターシンセがリッスンしている bus なので、2行目を評価するとすぐに、シンセ f によってフィルター処理されているホワイトノイズが聞こえ始めます。要するに、ルーティングは次のようになります。

noise synth→ bus 55→ filter synth

実行の順序は重要です。効果の前にソースを評価する場合、前の例は機能しません。これについては、セクション42"実行の順序"で詳しく説明します。最後の1つ:{SinOsc.ar(440)}.play のような以前のサンプルシンセで書いたとき、SCは実際に {Out.ar(0,SinOsc.ar(440))}.play の下で:bus 0 にサウンドを送信することを想定していたため、最初の UGen を自動的に Out.ar(0,...)UGen でラップしました。実際、舞台裏でさらにいくつかのことが行われていますが、これについては後で説明します(セクション39)。

31 マイク入力

下の例は、SoundIn UGen を使用してサウンドカードからサウンド入力に簡単にアクセスする方法を示しています。*


*In.ar は任意の bus から読み取り、サウンドカード入力はデフォルトで bus 8~15に割り当てられていることがわかっているため、In.ar(8) を書き込んでマイクからサウンドを取得できます。それはうまく機能しますが、SoundIn.ar はより便利なオプションです。

// 警告:フィードバックを避けるためにヘッドフォンを使用してください
{SoundIn.ar(0)}.play; // In.ar(8)と同じ:最初の入力バスからサウンドを取得

// ステレオバージョン
{SoundIn.ar([0, 1])}.play; // 1番目と2番目の入力
// ただの楽しみのためにいくつかのリバーブ?
{FreeVerb.ar(SoundIn.ar([0, 1]), mix: 0.5, room: 0.9)}.play;

32 マルチチャンネル拡張

メーターウィンドウが開いている状態で—[ctrl + M]—、これを見てください。

{Out.ar(0, Saw.ar(freq: [440, 570], mul: Line.kr(0, 1, 10)))}.play;

素晴らしい Line.kr UGen を使用して、振幅を10秒で0から1に増加させています。それはきちんとしている。しかし、ここではさらに興味深い魔法が起こっています。2つの出力チャネル(左と右)があることに気づきましたか?各チャンネルに異なる音があることを聞きましたか?そして、これらの2つの音符はリストから得られます— [440,570] —それは周波数引数として Saw.ar に渡されますか?

これはマルチチャネル拡張と呼ばれます。

David Cottle は、"マルチチャネル拡張はブードゥー教に隣接する[ 配列のapplication ]である" *と冗談を言います。これは、SuperCollider の最も強力でユニークな機能の1つであり、最初は人々を困惑させる可能性があります。 簡単に言うと、どこでも UGen の引数の1つとして配列を使用すると、パッチ全体が複製されます。作成されるコピーの数は、配列内のアイテムの数です。 これらの複製された UGen は、Out.ar の最初の引数として指定された bus から開始して、必要な数の隣接 bus に送信されます。


*Cottle, D. “Beginner’s Tutorial.” The SuperCollider Book, MIT Press, 2011, p. 14

上記の例では、Out.ar(0,...) があります。のこぎり波の周波数は、[440,570] の2つの項目の配列です。SCは何をしますか? "マルチチャネル拡張"により、パッチ全体のコピーが2つ作成されます。最初のコピーは、周波数440 Hzのノコギリ波で、バス0(左チャンネル)に送信されます。2番目のコピーは、周波数570 Hzのノコギリ波で、バス1(右チャンネル)に送信されます!

さあ、自分で確認してください。これらの2つの周波数を任意の他の値に変更します。結果を聞いてください。1つは左チャンネルに行き、もう1つは右チャンネルに行きます。さらに進んで、リストに3番目の頻度を追加します( [440,570,980] など)。メーターウィンドウを確認します。最初の3つの出力が点灯していることがわかります(ただし、マルチチャンネルサウンドカードを持っている場合のみ、3番目の出力を聞くことができます)。

さらに、同じ UGen の他の引数、または同じシンセの他の UGen の引数で追加の配列を使用できます。SuperCollider はハウスキーピングを行い、それに応じてこれらの値に従うシンセを生成します。たとえば、現在、両方の周波数[440,570] は10秒で0から1にフェードインしています。ただし、コードを Line.kr(0,1,[1,15]) に変更すると、440 Hzトーンがフェードインするのに1秒かかり、570 Hzトーンがフェードインするのに15秒かかります。試してみてください。

演習:古い電話の"ビジートーン"のシミュレーションを聞いてください。マルチチャンネル拡張を使用して、それぞれが異なるチャンネルで異なる周波数を再生する2つの正弦オシレーターを作成します。

左チャネルパルスを1秒間に2回、右チャネルパルスを1秒間に3回にします。7

a = {Out.ar(0, SinOsc.ar(freq: [800, 880], mul: LFPulse.ar(2)))}.play;
a.free;

33 Bus オブジェクト

前の2つのセクションで学んだすべてを使用する例は次のとおりです。オーディオバスとマルチチャネル拡張。

// これを最初に実行します("リバーブをオンにする"--最初は何も聞こえません)
r = {Out.ar(0, FreeVerb.ar(In.ar(55, 2), mix: 0.5, room: 0.9, mul: 0.4))}.play;

// ここでこの2番目を実行します("ビジートーンをリバーブバスにfeedする")
a = {Out.ar(55, SinOsc.ar([800, 880], mul: LFPulse.ar(2)))}.play;
a.free;

マルチチャネル拡張のおかげで、ビジートーンは2つのチャネルを使用します。 (シンセaで)ビジートーンを bus 55 にルーティングすると、2つの bus が実際に使い果たされます。bus 55 と、すぐ隣の bus 56 です。reverb (synth r) では、In.ar(55, 2) で bus 55 から始まる2つのチャンネルを読み取りたい場合:55と56の両方が reverb に入ります。reverb の出力も2つのチャンネルに拡張されるため、synth r は bus 0 と bus 1 (サウンドカードの左右のチャンネル)にサウンドを送信します。

今、ソースシンセをエフェクトシンセに接続するための bus 番号(55)の選択は任意でした: 16から127の間の他の番号でもかまいません( bus 0 から15はサウンドカードの出力と入力のために予約されています)。

bus 番号を自分でトラッキングしなければならないとしたら、どれほど不便でしょう。パッチが複雑になったらすぐに、悪夢を想像してください。"リバーブ用に再度選択した bus 番号は?59または95でしたか?遅延の bus 番号はどうですか?27だったと思う?思い出せない..." などなど。

SuperCollider は、Busオブジェクトを使用してこれを処理します。上記の例では、デモンストレーションのために悪名高い bus 55 のみを手動で割り当てました。 SuperCollider の日常生活では、シンプルに Bus オブジェクトを使用するべきです。 Bus オブジェクトは、常に使用可能な bus を選択して作業を行います。これがあなたの使い方です:

// バスを作成
~myBus = Bus.audio(s, 2);
// リバーブをオンにする:myBus(ソースサウンド)から読み取ります
r = {Out.ar(0, FreeVerb.ar(In.ar(~myBus, 2), mix: 0.5, room: 0.9, mul: 0.4))}.play;
// ビジートーンを~myBusにフィード 
b = {Out.ar(~myBus, SinOsc.ar([800, 880], mul: LFPulse.ar(2)))}.play;
// 両方のシンセを解放
r.free; b.free;

Bus.audio の最初の引数は、サーバーを表す変数sです。2番目の引数は、必要なチャネルの数です(例では2)。次に、それを意味のある名前の変数に入れます(例では~myBus ですが、~reverbBus、~source、~tangerine、またはパッチで意味のあるもの)。その後、そのバスを参照する必要があるときはいつでも、作成した変数を使用します。

34 パンニング

パンニングとは、オーディオ信号をステレオまたはマルチチャンネルの音場に広げることです。以下は、Pan2 によって左右のチャンネル間で跳ね返るモノ信号です。*

p = {Pan2.ar(in: PinkNoise.ar, pos: SinOsc.kr(2), level: 0.1)}.play;
p.free;

Pan2 ヘルプファイルから、引数pos(位置)が-1(左)から+1(右)の間の数値を期待し、0が中心であることがわかります。そのため、SinOscを直接その引数に使用できます。sine oscillator はバイポーラ UGenであるため、デフォルトで-1〜+1の数値を出力します。


*マルチチャンネルパンニングについては、Pan4 および PanAz をご覧ください。上級ユーザーは、Ambisonics の SuperCollider プラグインをご覧ください。

以下に、より詳細な例を示します。のこぎり波は、非常に鋭いバンドパスフィルターを通過します(rq:0.01)。ローカル変数を使用して、コードのさまざまな部分をモジュール化することに注意してください。 上記の例でできる限り分析して理解しようとしましょう。次に、以下の質問に答えてください。

(
x = {
    var lfn = LFNoise2.kr(1);
    var saw = Saw.ar(
    freq: 30,
    mul: LFPulse.kr(
    freq: LFNoise1.kr(1).range(1, 10),
    width: 0.1));
    var bpf = BPF.ar(in: saw, freq: lfn.range(500, 2500), rq: 0.01, mul: 20);
    Pan2.ar(in: bpf, pos: lfn);
}.play;
)
x.free;

質問:

(a)変数 lfn は2つの異なる場所で使用されます。どうして? (結果はどうなりますか?) (b)BPFのmul:引数を20から10、5、または1に変更するとどうなりますか? なぜ20という高い数値が使用されたのですか? (c)コードのどの部分がリズムを制御していますか?

回答はこのドキュメントの最後にあります。8

35 Mix と Splay

ここにクールなトリックがあります。マルチチャンネル拡張を使用して複雑なサウンドを生成し、それをすべて Mix または Splay でモノラルまたはステレオにミックスダウンできます:

// 5チャネル出力(メーターウィンドウを見てください)
a = { SinOsc.ar([100, 300, 500, 700, 900], mul: 0.1) }.play;
a.free;
// monoにミックスダウン:
b = { Mix(SinOsc.ar([100, 300, 500, 700, 900], mul: 0.1)) }.play;
b.free;
// ステレオにミックスダウンします(左から右に均等に)
c = { Splay.ar(SinOsc.ar([100, 300, 500, 700, 900], mul: 0.1)) }.play;
c.free
// Splay を楽しむ:
(
d = {arg fundamental = 110;
    var harmonics = [1, 2, 3, 4, 5, 6, 7, 8, 9];
    var snd = BPF.ar(
        in: Saw.ar(32, LFPulse.ar(harmonics, width: 0.1)),
        freq: harmonics * fundamental,
        rq: 0.01,
        mul: 20);
    Splay.ar(snd);
}.play;
)
d.set(\fundamental, 100); // fundamental を変えてみます
d.free;

最後の Splay の例で、マルチチャンネル拡張が機能しているのを見ることができますか? 唯一の違いは、UGen で使用される前に、配列が最初に変数(harmonics)に格納されることです。array harmonics には9つのアイテムがあるため、シンセは9チャンネルに拡張されます。次に、.play の直前に、Splay は9つのチャンネルの配列を取り込み、ステレオにミックスダウンして、チャンネルを左から右に均等に展開します。*

Mix にはもう1つの素晴らしいトリックがあります:メソッド fill です。シンセの配列を作成し、それを一度にモノラルにミックスダウンします。

// インスタントクラスタージェネレーター
c = { Mix.fill(16, {SinOsc.ar(rrand(100, 3000), mul: 0.01)}) }.play;
c.free;
// A note with 12 partials of decreasing amplitudes
(
n = { Mix.fill(12, {arg counter;
    var partial = counter + 1; // 0ではなく1から開始したい
    SinOsc.ar(partial * 440, mul: 1/partial.squared) * 0.1
    })
}.play;
FreqScope.new;
)
n.free;

Mix.fill には、作成する配列のサイズと、配列を埋めるために使用される関数(中括弧)の2つのことを指定します。上記の最初の例では、Mix.fill は関数を16回評価します。関数には変数が含まれていることに注意してください。sine オシレーターの周波数は、100から3000の間の任意の乱数にすることができます。それぞれが異なるランダム周波数を持つ16の正弦波が作成されます。それらはすべてモノラルにミックスダウンされ、結果が左チャンネルで聞きこえます。2番目の例は、関数が( Array.fill のように)反復回数を追跡する counter 引数を取ることができることを示しています。

12個の sine オシレーターがハーモニックシリーズに続いて生成され、モノラルで単一のノートにミックスダウンされます。


*.play の前の最後の行は、Out.ar(0, Splay.ar(snd)) として明示的に記述できます。SuperCollider が丁寧にギャップを埋め、そこに Out.ar(0...) を投入していることを忘れないでください。これは、シンセが、左 (bus 0) と右 (bus 1) のチャンネルで再生する必要があることを認識していることです。

36 Playing an audio file

まず、サウンドファイルをバッファにロードする必要があります。Buffer.read の2番目の引数は、二重引用符 "" で囲まれたサウンドファイルのパスです。コンピュータ上のWAVまたはAIFFファイルを指すように、それに応じて変更する必要があります。バッファがロードされたら、PlayBuf UGen を使用してさまざまな方法でバッファを再生します。

ヒント:bird2.PNG
コンピューターに保存されているサウンドファイルの正しいパスを取得する簡単な方法は、ファイルを空のSuperColliderドキュメントにドラッグすることです。SCは既に二重引用符で囲まれた完全なパスを自動的に提供します!
// buffersにロードします:
~buf1 = Buffer.read(s, "/home/Music/wheels-mono.wav"); // 1つのサウンドファイル
~buf2 = Buffer.read(s, "/home/Music/mussorgsky.wav"); // 別のサウンドファイル

// プレイバック:
{PlayBuf.ar(1, ~buf1)}.play; // number of channels and buffer
{PlayBuf.ar(1, ~buf2)}.play;

// ファイルの情報を得る:
[~buf1.bufnum, ~buf1.numChannels, ~buf1.path, ~buf1.numFrames];
[~buf2.bufnum, ~buf2.numChannels, ~buf2.path, ~buf2.numFrames];

// 'rate'によってプレイバックスピードを変える
{PlayBuf.ar(numChannels: 1, bufnum: ~buf1, rate: 2, loop: 1)}.play;
{PlayBuf.ar(1, ~buf1, 0.5, loop: 1)}.play; // 半分の速度でプレイする
{PlayBuf.ar(1, ~buf1, Line.kr(0.5, 2, 10), loop: 1)}.play; // スピードアップ
{PlayBuf.ar(1, ~buf1, MouseY.kr(0.5, 3), loop: 1)}.play; // マウスコントロールで

// 方向を変える(reverse)
{PlayBuf.ar(1, ~buf2, -1, loop: 1)}.play; // 逆再生reverse sound
{PlayBuf.ar(1, ~buf2, -0.5, loop: 1)}.play; // play 半分のスピードで逆再生

37 Synth Nodes

前の PlayBuf の例では、各行の後に [ctrl+.] を押してサウンドを停止する必要がありました。他の例では、シンセを変数( x = {WhiteNoise.ar}.playなど)に割り当てて、x.free で直接停止できるようにしました。 SuperCollider でシンセを作成するたびに、「サウンドエンジン」であるサーバーで実行されることがわかります。サーバーで実行されている各シンセは、ノードで表されます。コマンド s.plotTree を使用して、このノードツリーを覗くことができます。それを試してみてください。NodeTree という名前のウィンドウが開きます。

// GUI をオープン
s.plotTree;
// これらを1つずつ実行し(サウンドを停止しないでください)、ノードツリーを監視します:
w = { SinOsc.ar(60.midicps, 0, 0.1) }.play;
x = { SinOsc.ar(64.midicps, 0, 0.1) }.play;
y = { SinOsc.ar(67.midicps, 0, 0.1) }.play;
z = { SinOsc.ar(71.midicps, 0, 0.1) }.play;
w.free;
x.free;
y.free;
z.free;

ノードツリーに表示されるすべての長方形は、シンセノードです。各シンセには一時的な名前(temp_101、temp_102など)が付けられ、実行中はそこにとどまります。4つのサインをもう一度再生して、 [ctrl+.] を押します(ノードツリーウィンドウを確認します)。ショートカット [ctrl+.] は、サーバーで実行されているすべてのノードを一度に無慈悲に停止します。一方、.free メソッドを使用すると、よりこまかに、特定のノードを1つずつ解放できます。

重要なことの1つは、無音のみを生成している場合でも、シンセがサーバーで実行されたままになる可能性があることです。以下に例を示します。この WhiteNoise UGen の振幅は、2秒で0.2から0になります。その後、何も聞こえません。しかし、シンセノードはまだそこにあり、解放するまで消えません。

1 // ノードツリーウィンドウを数秒間監視する
2 x = {WhiteNoise.ar(Line.kr(0.2, 0, 2))}.play;
3 x.free;

37.1 The glorious doneAction: 2

幸いなことに、シンセをよりスマートにする方法があります。たとえば、Line.kr にジョブが終了したとき(0.2から0へのランプ)に通知するようにLine.kr に依頼できると便利ですよね?そうなればシンセは自動的に解放されますね。 引数 doneAction: 2 を入力して、すべての問題を解決します。 以下の例を再生し、doneAction: 2 を使用した場合と使用しない場合の動作を比較します。

行を実行しながらノードツリーを見てみましょう。

// doneAction: 2 無しで
{WhiteNoise.ar(Line.kr(0.2, 0, 2))}.play;
{PlayBuf.ar(1, ~buf1)}.play; // PS. これは、まだ前のセクションを引き継いで ~buf1 にサウンドファイルが読み込まれていることを前提としています

// doneAction: 2 を使います
{WhiteNoise.ar(Line.kr(0.2, 0, 2, doneAction: 2))}.play;
{PlayBuf.ar(1, ~buf1, doneAction: 2)}.play;

doneAction: 2 のシンセは、ジョブが完了するとすぐに(つまり、最初の例で Line.kr ランプが終了し、2番目の例のサウンドファイルの PlayBuf.ar が再生を終了するとすぐに)自動的に解放されます。この知識は、次のセクション「エンベロープ」で非常に役立ちます。

38 エンベロープ

これまでの例のほとんどは連続音でした。音の振幅エンベロープを形作る方法を学ぶ時が来ました。始めるのに良い例は、パーカッシブ エンベロープです。

シンバルのクラッシュを想像してください。音が無音から最大振幅になるまでにかかる時間は非常に短く、おそらく数ミリ秒です。これは、attack time と呼ばれます。シンバルの音が最大振幅から無音(ゼロ)に戻るまでにかかる時間は、もう少し長く、おそらく数秒です。これは release time と呼ばれます。

振幅エンベロープは、単に音を生成する UGen の乗数(mul)として使用される時間とともに変化する数値と考えてください。これらの数値は、SuperCollider が振幅を理解する方法であるため、0(無音)から1(フルアンプ)の間である必要があります。

これまでに、最後の例には既に振幅エンベロープが含まれていることに気付いたかもしれません。 {WhiteNoise.ar(Line.kr(0.2, 0, 2, doneAction: 2))}.play によって、2秒間でホワイトノイズの振幅を0.2から0に変更します。ただし、Line.kr は、非常に柔軟なタイプのエンベロープではありません。

Env は、あらゆる種類のエンベロープを定義するために常に使用するオブジェクトです。Env には多くの便利なメソッドがあります。ここでは少しだけ見ます。詳細については、Env ヘルプファイルをご覧ください。

38.1 Env.perc

Env.perc は、パーカッシブなエンベロープを取得する便利な方法です。それは、attackTime、releaseTime、level、および curve の4つの引数を取ります。シンセ以外の典型的なシェイプを見てみましょう。

Env.perc.plot; // すべてのデフォルト引数を使用
Env.perc(0.5).plot; // attackTime: 0.5
Env.perc(attackTime: 0.3, releaseTime: 2, level: 0.4).plot;
Env.perc(0.3, 2, 0.4, 0).plot; // 上と同じ、ただし、curve:0は直線を意味します

これで、次のようなシンセに簡単に接続できます:

{PinkNoise.ar(Env.perc.kr(doneAction: 2))}.play; // デフォルト Env.perc 引数
{PinkNoise.ar(Env.perc(0.5).kr(doneAction: 2))}.play;
{PinkNoise.ar(Env.perc(0.3, 2, 0.4).kr(2))}.play;
{PinkNoise.ar(Env.perc(0.3, 2, 0.4, 0).kr(2))}.play;

Env.perc の直後に .kr(doneAction: 2) を追加するだけで、準備完了です。実際、この場合は doneAction の明示的な宣言を削除して、シンプルに .kr(2) を使用することもできます。.kr は、SCにこのエンベロープをコントロール レートで「実行」するように伝えています(これより前で見た他のコントロール レート信号と同様)。

38.2 Env.triangle

Env.triangle は、duration, level の2つの引数のみを取ります。

例:

// これを見て:
Env.triangle.plot;
// これを聴いて:
{SinOsc.ar([440, 442], mul: Env.triangle.kr(2))}.play;
// ところで、エンベロープはコードの任意の場所で乗数にすることができます
{SinOsc.ar([440, 442]) * Env.triangle.kr(2)}.play;

38.3 Env.linen

Env.linen は、アタック、サステイン部分、およびリリースを含むラインエンベロープについて説明しています。レベルと曲線の種類も指定できます。 例:

// これを見て: 
Env.linen.plot; 
// これを聴いて: 
SinOsc.ar([300, 350], mul: Env.linen(0.01, 2, 1,
0.2).kr(2))}.play;

38.4 Env.pairs

さらに柔軟性が必要ですか? Env.pairs を使用すると、任意の形状と期間のエンベロープを作成できます。 Env.pairs は、2つの引数を取ります。[time, level] ペアの配列、および曲線のタイプ(使用可能なすべての曲線タイプについては、Env Helpファイルを参照してください)。

(
{
    var env = Env.pairs([[0, 0], [0.4, 1], [1, 0.2], [1.1, 0.5], [2, 0]], \lin);
    env.plot;
    SinOsc.ar([440, 442], mul: env.kr(2));
}.play;
)

次のようなペアの配列を読み取ります:

​ At time 0, be at level 0;

​ At time 0.4, be at level 1;

​ At time 1, be at level 0.2;

​ At time 1.1, be at level 0.5;

​ At time 2, be at level 0.

38.4.1 Envelopes—not just for amplitude

これらの同じシェイプを使用して振幅以外の何かをコントロールすることを妨げるものは何もありません。 必要な数値の範囲に合わせてスケーリングする必要があります。 たとえば、エンベロープを作成して、時間の経過に伴う周波数の変化をコントロールできます。

(
{
    var freqEnv = Env.pairs([[0, 100], [0.4, 1000], [0.9, 400], [1.1, 555], [2,
440]], \lin);
    SinOsc.ar(freqEnv.kr, mul: 0.2);
}.play;
)

エンベロープは、時間の経過とともに変化する必要があるシンセパラメーターをコントロールする強力な方法です。

38.5 ADSR Envelope

これまでに表示されたすべてのエンベロープには、1つの共通点があります。それらは、事前定義された固定の duration を持っています。ただし、このタイプのエンベロープが適切でない場合があります。たとえば、MIDIキーボードで演奏しているとします。キーを押すと、ノートの attack がトリガーされます。リリースは、キーから指を離したときです。ただし、事前には指がキー押している時間はわかりません。この場合に必要なのは、いわゆる“持続エンベロープ”です。

ASR(アタック、サステイン、リリース)がエンベロープに適合します。より一般的なバリエーションは、ADSRエンベロープ( Attack、Decay、Sustain、Release )です。両方見てみましょう。

// ASR
// 音をプレイ ('press key')
// attackTime: 0.5 seconds, sustainLevel: 0.8, releaseTime: 3 seconds
x = {arg gate = 1, freq = 440; SinOsc.ar(freq: freq, mul: Env.asr(0.5, 0.8, 3).kr(
    doneAction: 2, gate: gate))}.play;
// 音を止める ('finger off the key' 􀀀 activate release stage)
x.set(\gate, 0); // alternatively, x.release

// ADSR (attack, decay, sustain, release)
// 音をプレイ:
(
d = {arg gate = 1;
    var snd, env;
    env = Env.adsr(0.01, 0.4, 0.7, 2);
    snd = Splay.ar(BPF.ar(Saw.ar((32.1, 32.2..33)), LFNoise2.kr(12).range(100,1000), 0.05, 10));
    Out.ar(0, snd * env.kr(doneAction: 2, gate: gate));
}.play;
)
// 音を止める:
d.release; // これは d.set(\gate, 0); 同等です

Key concepts:

Attack ゼロ(無音)からピーク振幅までにかかる時間(秒)

Decay ピーク振幅からサステイン振幅まで下降するのにかかる時間(秒)

Sustain 音を保持する振幅(0〜1)(重要:これは時間とは関係ありません)

Release サステインレベルからゼロ(無音)になるまでにかかる時間(秒単位)。

持続エンベロープには事前にわかっている total duration がないため、いつスタートする( attack をトリガーする)か、いつ停止する( release をトリガーする)かを通知する必要があります。この通知は gate と呼ばれます。gate は、エンベロープに"開く"(1)と"閉じる"(0)を指示するものであり、音を開始および停止します。

ASRまたはADSRエンベロープをシンセで機能させるには、gate 引数を宣言する必要があります。通常、シンセの再生をすぐに開始するため、デフォルトは gate = 1 です。シンセを停止したい場合は、単に .release または .set(\gate,0) メッセージを送信します。エンベロープのリリース部分がトリガーされます。たとえば、リリース時間が3の場合、メッセージ .set(\gate,0) を送信した瞬間から音が消えるまで3秒かかります。

38.6 EnvGen

記録については、以下のコードに示すように、エンベロープを生成するためにこのセクションで学んだ構成はショートカット(短縮)であることを知っておく必要があります。

// これは:
{ SinOsc.ar * Env.perc.kr(doneAction: 2) }.play;
// ... これのショートカットです:
{ SinOsc.ar * EnvGen.kr(Env.perc, doneAction: 2) }.play;

EnvGen は、Env によって定義されたブレークポイントエンベロープを実際に再生する UGen です。すべての実用的な目的のために、ショートカット(短縮形)を引き続き使用できます。ただし、ヘルプファイルやその他のオンライン例で EnvGen が使用されていることがよくあるので、これらの表記は同等であることを知っておくと便利です。

39 シンセの定義

これまで、シンセをシームレスに定義し、すぐに再生してきました。さらに、.set メッセージにより、シンセコントロールをリアルタイムで変更できる柔軟性が得られました。ただし、シンセを最初に定義し(すぐに再生せずに)、後でのみ再生したい場合があります。つまり、レシピを書き留める瞬間(シンセの定義)とケーキを焼く瞬間(サウンドを作成する)を分離する必要があるということです。

39.1 SynthDef と Synth

SynthDef は、シンセの"レシピを書く"ために使用するものです。その後、Synth で再生できます。以下に簡単な例を示します。

// SynthDefオブジェクトを使用したシンセ定義
SynthDef("mySine1", {Out.ar(0, SinOsc.ar(770, 0, 0.1))}).add;
// Synthオブジェクトで音をプレイする
x = Synth("mySine1");
x.free;

// 引数を使用したもう少し柔軟な例
// and 自己終了エンベロープ (doneAction: 2)
SynthDef("mySine2", {arg freq = 440, amp = 0.1;
    var env = Env.perc(level: amp).kr(2);
    var snd = SinOsc.ar(freq, 0, env);
    Out.ar(0, snd);
}).add;

Synth("mySine2"); // デフォルト値を使用する
Synth("mySine2", [\freq, 770, \amp, 0.2]);
Synth("mySine2", [\freq, 415, \amp, 0.1]);
Synth("mySine2", [\freq, 346, \amp, 0.3]);
Synth("mySine2", [\freq, rrand(440, 880)]);

SynthDef の最初の引数は、シンセのユーザー定義名です。 2番目の引数は UGen グラフを指定する関数です(これが UGen の組み合わせが呼び出される方法です)。Out.ar を明示的に使用して、信号を送信するバスを指定する必要があることに注意してください。最後に、SynthDef はメッセージ .add を最後に受け取ります。つまり、SCが認識しているシンセのコレクションに追加することになります。

これは、SuperCollider を終了するまで有効です。 SynthDef で1つ以上のシンセ定義を作成した後、Synth でそれらを再生できます。

最初の引数は使用するシンセ定義の名前で、2番目の(オプショナル)引数は指定したいパラメーター( freq、amp など)を持つ配列です。

39.2 Example

これはもっと長い例です。SynthDef を追加した後、array トリックを使用して、ランダムなピッチと振幅で6音のコードを作成します。各シンセは array のスロットの1つに格納されているため、個別にリリースできます。

// SynthDefを作成する
(
SynthDef("wow", {arg freq = 60, amp = 0.1, gate = 1, wowrelease = 3;
var chorus, source, filtermod, env, snd;
chorus = Lag.kr(freq, 2) * LFNoise2.kr([0.4, 0.5, 0.7, 1, 2, 5, 10]).range(1,1.02);
source = LFSaw.ar(chorus) * 0.5;
filtermod = SinOsc.kr(1/16).range(1, 10);
    env = Env.asr(1, amp, wowrelease).kr(2, gate);
    snd = LPF.ar(in: source, freq: freq * filtermod, mul: env);
Out.ar(0, Splay.ar(snd))
}).add;
)

// ノードツリーを見る
s.plotTree;

// 6音の和音を作成する
a = Array.fill(6, {Synth("wow", [\freq, rrand(40, 70).midicps, \amp, rrand(0.1, 0.5)
])}); // all in a single line

// 1つ1つ音をリリース
a[0].set(\gate, 0);
a[1].set(\gate, 0);
a[2].set(\gate, 0);
a[3].set(\gate, 0);
a[4].set(\gate, 0);
a[5].set(\gate, 0);

// ADVANCED: 6音のコードを再度実行して、この行を評価します.
// 何が起きているのか理解できますか?
SystemClock.sched(0, {a[5.rand].set(\freq, rrand(40, 70).midicps); rrand(3, 10)});

上記の SynthDef を理解するために: * 結果として得られるサウンドは、ローパスフィルターを通過する7つの厳密に調整されたノコギリ波オシレーターの合計です。 * これらの7つのオシレーターは、マルチチャンネル拡張により作成されます。

  • 変数 chorus とは何ですか? 周波数 freq に LFNoise2.kr を掛けたものです。7項目の配列が LFNoise2 の引数として指定されているため、マルチチャネル拡張がここから始まります。その結果、LFNoise2 のコピーが7つ作成され、各コピーはリスト [0.4, 0.5, 0.7, 1, 2, 5, 10] から取得した異なる速度で実行されます。出力は、1.0〜1.02の範囲に制限されます。

  • 追加機能として、freq が Lag.kr で囲まれていることに注意してください。このシンセに新しい周波数値が入力されるたびに、Lag UGenは古い値と新しい値の間のramp を作成します。この場合、「遅延時間」(ramp の持続時間)は2秒です。これが、例の最後の行を実行した後に聞こえるグリッサンド効果になります。

  • ソースサウンド LFSaw.ar は、周波数として変数 chorus を使用します。具体的な例:60 Hzの周波数値の場合、変数 chorus は次のような式になります

    60 * [1:01; 1:009; 1:0; 1:02; 1:015; 1:004; 1:019]
    

  • リスト内数値は、各 LFNoise2 の速度に応じて常に上下に変化します。最終結果は、常に 60〜61.2 (60 * 1.02) の間でスライドする7つの周波数のリストです。これはコーラスエフェクトと呼ばれます、また変数名(chorus)です。

  • 変数 chorus を LFSaw.ar の freq として使用すると、マルチチャンネル拡張が発生します。これで、わずかに異なる周波数を持つ7つのノコギリ波ができました。

  • 変数 filtermod は、非常にゆっくり(16秒間に1サイクル)動く正弦波 oscillator であり、出力範囲は1〜10にスケーリングされます。これは、ローパスフィルターのカットオフ周波数を変調するために使用されます。

  • 変数 snd は、ローパスフィルター(LPF)を保持します。LPF は、入力としてソースを取得し、カットオフ周波数を超えるすべての周波数をフィルター処理します。このカットオフは固定値ではなく、式 freq * filtermod です。したがって、freq = 60 を想定した例では、これは60〜600の数値になります。filtermod は1〜10の間で振動する数値であるため、乗算は 60 *(1〜10) になります。

  • LPF は、マルチチャネルも7つのコピーに拡張します。振幅エンベロープ env もそこに適用されます。

  • 最後に、Splay はこの7つのチャンネルの配列を取り、ステレオにミックスダウンします。

39.3 Under the hood

最初に SynthDef を作成し(一意の名前で)、次に aSynth を呼び出すというこの2段階のプロセスは、{SinOsc.ar} .play のような単純なステートメントを記述するときにSCが常に行うことです。 SuperCollider は、それを(a)一時的な SynthDef の作成、および(b)即時再生(したがって、Post ウィンドウに表示される temp_01、temp_02 という名前)に解凍(unpacks)します。あなたの便宜のために、それらはすべて舞台裏で。

// これをするとき:
{SinOsc.ar(440)}.play;
// SCがやっているのはこれ:
{Out.ar(0, SinOsc.ar(440))}.play;
// 実際にはこういうことです:
SynthDef("tempName", {Out.ar(0, SinOsc.ar(440))}).play;

// そして、それらはすべて、この2ステップ操作へのショートカットです:
SynthDef("tempName", {Out.ar(0, SinOsc.ar(440))}).add; // シンセ定義を作成する
Synth("tempName"); // play it

40 Pbind can play your SynthDef

SynthDefs としてシンセを作成することの美しさの1つは、Pbind を使用してそれらを再生できることです。 "wow" SynthDef がまだメモリにロードされていると仮定して(最後の例の後でSCを終了して再度開いた場合を除き)、以下の Pbinds を試してください:

(
Pbind(
\instrument, "wow",
\degree, Pwhite(-7, 7),
\dur, Prand([0.125, 0.25], inf),
\amp, Pwhite(0.5, 1),
\wowrelease, 1
).play;
)


(
Pbind(
\instrument, "wow",
\scale, Pstutter(8, Pseq([
Scale.lydian,
Scale.major,
Scale.mixolydian,
Scale.minor,
Scale.phrygian], inf)),
\degree, Pseq([0, 1, 2, 3, 4, 5, 6, 7], inf),
\dur, 0.2,
\amp, Pwhite(0.5, 1),
\wowrelease, 4,
\legato, 0.1
).play;
)

Pbind を使用してカスタム SynthDef の1つを再生するときは、次の点に注意してください:

  • Pbind key\instrumentを使用して、あなたの SynthDef の名前を宣言します。

  • SynthDef のすべての引数は、Pbind 内部からアクセスおよび制御できます。 それらを Pbind キーとして使用するだけです。たとえば、上記で使用した \wowrelease という引数に注目してください。wow これは Pbind が理解するデフォルトのキーの1つではなく、シンセ定義の固有のキーです(変な名前は意図的に選択されています)。

  • Pbind のすべてのピッチ変換機能( \degree、\note、および \midinote のキー)を使用するには、SynthDef に freq の引数入力があることを確認します(そのように正確に入力する必要があります)。Pbind が計算を行います。

  • Env.adsr などサスティーエンベロープを使用する場合、シンセにデフォルトの引数 gate = 1 が設定されていることを確認してください( Pbind は舞台裏で適切なタイミングでノートを停止するため、そのように正確に入力する必要があります)。

  • サスティーエンベロープを使用していない場合、SynthDef に適切な UGen に doneAction:2 が含まれていることを確認して、サーバー内のシンセノードを自動的に解放します。

演習:1つ以上の Pbind を作成して、以下に示す "pluck" SynthDef を再生します。mutedString 引数には、0.1〜0.9の値を試してください。Pbind の1つに、ゆっくりしたコードのシーケンスを再生させます。\strum で和音をアルペジエイトしてみてください。

(
SynthDef("pluck", {arg amp = 0.1, freq = 440, decay = 5, mutedString = 0.1;
var env, snd;
env = Env.linen(0, decay, 0).kr(doneAction: 2);
snd = Pluck.ar(
    in: WhiteNoise.ar(amp),
    trig: Impulse.kr(0),
    maxdelaytime: 0.1,
    delaytime: freq.reciprocal,
    decaytime: decay,
    coef: mutedString);
    Out.ar(0, [snd, snd]);
}).add;
)

41 コントロール Bus

このチュートリアルの前半で、オーディオ bus(セクション30)とバスオブジェクト(セクション33)について説明しました。オーディオルーティングの概念に焦点を合わせるために、当時のコントロールバスのトピックは別としておきました。 SuperCollider のコントロール bus は、オーディオではなくコントロール信号をルーティングするためのものです。この違いを除いて、オーディオ bus とコントロール bus の間に実用的または概念的な違いはありません。

コントロール bus の作成と管理は、オーディオバスで行うのと同じ方法で、.ar の代わりに .kr を使用するだけです。SuperCollider には、デフォルトで4096個のコントロール bus があります。 以下の例の最初の部分では、デモンストレーションのためだけに任意の bus 番号を使用します。2番目の部分では、 bus の作成に推奨されるBusオブジェクトを使用します。

// コントロール bus 55にコントロール信号を書き込む
{Out.kr(55, LFNoise0.kr(1))}.play;
// コントロール bus 55からコントロール信号を読み取る
{In.kr(55).poll}.play;

// Busオブジェクトの使用
~myControlBus = Bus.control(s, 1);
{Out.kr(~myControlBus, LFNoise0.kr(5).range(440, 880))}.play;
{SinOsc.ar(freq: In.kr(~myControlBus))}.play;

次の例は、2つの異なるシンセを同時に変調するために使用される単一のコントロール信号を示しています。Blip シンセでは、制御信号は1から10までの倍音の数をコントロールするために再スケーリングされます。2番目のシンセでは、同じコントロール信号がパルスオシレータの周波数を変調するために再スケーリングされます。

// コントロール bus を作成する
~myControl = Bus.control(s, 1);

// コントロール信号を bus に送ります
c = {Out.kr(~myControl, Pulse.kr(freq: MouseX.kr(1, 10), mul: MouseY.kr(0, 1)))}.play;

// コントロールされている音を再生する
// (マウスを動かして変更を聞く)
(
{
    Blip.ar(
        freq: LFNoise0.kr([1/2, 1/3]).range(50, 60),
        numharm: In.kr(~myControl).range(1, 10),
        mul: LFTri.kr([1/4, 1/6]).range(0, 0.1))
}.play;

{
    Splay.ar(
        Pulse.ar(
            freq: LFNoise0.kr([1.4, 1, 1/2, 1/3]).range(100, 1000) * In.kr(~myControl).range(0.9, 1.1),
            mul: SinOsc.ar([1/3, 1/2, 1/4, 1/8]).range(0, 0.03))
)
}.play;
)

// 比較するコントロール信号をオフにする
c.free;

41.1 asMap

次の例では、メソッド asMap を使用して、制御バスを実行中のシンセノードに直接マップします。この方法では、シンセの定義に In.kr も必要ありません。

// SynthDef を作成する
SynthDef("simple", {arg freq = 440; Out.ar(0, SinOsc.ar(freq, mul: 0.2))}).add;
// コントロール bus を作成する
~oneBus = Bus.control(s, 1);
~anotherBus = Bus.control(s, 1);
// Start controls
{Out.kr(~oneBus, LFSaw.kr(1).range(100, 1000))}.play;
{Out.kr(~anotherBus, LFSaw.kr(2, mul: -1).range(500, 2000))}.play;
// Start a note
x = Synth("simple", [\freq, 800]);
x.set(\freq, ~oneBus.asMap);
x.set(\freq, ~anotherBus.asMap);
x.free;

42 実行の順序

セクション30でオーディオバスについて議論するとき、実行順序の重要性を示唆しました。 以下のコードは、セクション30のフィルター処理されたノイズの例の拡張バージョンです。以下の説明では、実行順序の基本概念を説明し、実行順序が重要である理由を示します。

// audio bus をつくる
~fxBus = Bus.audio(s, 1);
~masterBus = Bus.audio(s, 1);
// SynthDef をつくる
(
    SynthDef("noise", {Out.ar(~fxBus, WhiteNoise.ar(0.5))}).add;
    SynthDef("filter", {Out.ar(~masterBus, BPF.ar(in: In.ar(~fxBus), freq: MouseY.kr
(1000, 5000), rq: 0.1))}).add;
SynthDef("masterOut", {arg amp = 1; Out.ar(0, In.ar(~masterBus) * Lag.kr(amp, 1))}).
add;
)
// ノードツリーウィンドウを開く:
s.plotTree;
// synth をプレイ(ノードツリーを見ます)
m = Synth("masterOut");
f = Synth("filter");
n = Synth("noise");
// マスターボリューム
m.set(\amp, 0.1);

最初に、変数 ~fxbus および ~masterBus に割り当てられた2つのオーディオ bus 。 次に、3つの SynthDef が作成されます:

  • 「ノイズ」は、ホワイトノイズをエフェクト bus に送信するノイズソースです;

  • 「フィルター」は、エフェクト bus から入力を受け取り、処理されたサウンドをマスター bus に送信するバンドパスフィルターです;

  • 「masterOut」は、マスター bus からの信号を受け取り、単純なボリュームコントロールを適用して、音量を調整した最終的なサウンドをスピーカーに送信します;

シンセを順番に実行しながらノードツリーを確認します。 [ノードツリー]ウィンドウのシンセノードは、上から下に実行されます。デフォルトでは、最新のシンセがトップに追加されます。図9では、「ノイズ」が上、「フィルター」が2番目、「masterOut」が最後になっています。これが正しい順序です。上から下に順に読みこみ、ノイズソースはフィルターに流れ込み、フィルターの結果はマスターバスに流れ込みます。

Figure09.png 図9: Synth nodes in the Node Tree window

サンプルをもう一度実行して、行 m、f、および n を逆の順序で評価しようとすると、信号が間違った順序で計算されるため、何も聞こえません。

正しい順序で正しい行を評価することは問題ありませんが、コードが複雑になると、注意が必要になる場合があります。この job を簡単にするために、 SuperCollider では、ノードツリーのシンセを配置する場所を明示的に定義できます。このために、target 引数と addAction 引数を使用します。

n = Synth("noise", addAction: 'addToHead');
m = Synth("masterOut", addAction: 'addToTail');
f = Synth("filter", target: n, addAction: 'addAfter');

これで、上記の行をどの順序で実行しても、ノードが正しい場所に確実に収まるようになります。「ノイズ」シンセは、ノードツリーの先頭に追加するように明示的に指示されます。「masterOut」がテールに追加されます; フィルタはターゲット n の直後に明示的に追加されます(103ノイズシンセ)。

42.1 Groups

たくさんのシンセサイザーを使い始めたら、そのうちのいくつかはソースサウンド用、その他はエフェクト用、または必要なものは何でも、それらをグループにまとめることをお勧めします。これが基本的な例です:

// NodeTree のすべてを監視し続ける
s.plotTree;
// busをつくります
~reverbBus = Bus.audio(s, 2);
~masterBus = Bus.audio(s, 2);
// groups を定義
(
~sources = Group.new;
~effects = Group.new(~sources, \addAfter);
~master = Group.new(~effects, \addAfter);
)
// いっせいにすべてのシンセを実行
(
// One source sound
{
Out.ar(~reverbBus, SinOsc.ar([800, 890])*LFPulse.ar(2)*0.1)
}.play(target: ~sources);
// Another source sound
{
Out.ar(~reverbBus, WhiteNoise.ar(LFPulse.ar(2, 1/2, width: 0.05)*0.1))
}.play(target: ~sources);
// reverb です
{
Out.ar(~masterBus, FreeVerb.ar(In.ar(~reverbBus, 2), mix: 0.5, room: 0.9))
}.play(target: ~effects);
// マウスを使った愚かなマスターボリュームコントロール
{Out.ar(0, In.ar(~masterBus, 2) * MouseY.kr(0, 1))}.play(target: ~master);
)

実行順序の詳細については、ヘルプファイル「Synth」、「Order of Execution」、および「Group」を参照してください。

Part V

WHAT’S NEXT?

これまでにこのチュートリアルのすべてを読んで理解していれば、あなたはもはや SuperCollider の初心者ではありません! 私たちは多くの分野をカバーしました。ここからは、個人プロジェクトの開発を開始し、独力で学習を続けるために必要なすべての基本的なツールを手に入れました。以下のセクションでは、いくつかの一般的な中級レベルのトピックについて簡単に紹介します。最後のセクションでは、他のチュートリアルと学習リソースの簡潔なリストを示します。

43 MIDI

MIDIの拡張プレゼンテーションコンセプトや技は、このチュートリアルの範囲外です。以下の例は、MIDIバイスにある程度精通していることを前提として、スタートするために提供されています。

// 利用可能なすべてのデバイスをSCに接続する簡単な方法
MIDIIn.connectAll;

// すべての受信MIDIメッセージをすばやく表示する方法
MIDIFunc.trace(true);
MIDIFunc.trace(false); // stop it

// すべてのCC入力をすばやく検査する方法
MIDIdef.cc(\someCC, {arg a, b; [a, b].postln});

// CC 7、チャンネル0からのみ入力を取得
MIDIdef.cc(\someSpecificControl, {arg a, b; [a, b].postln}, ccNum: 7, chan: 0);

// 簡単なテストのための SynthDef
SynthDef("quick", {arg freq, amp; Out.ar(0, SinOsc.ar(freq) * Env.perc(level: amp).
kr(2))}).add;

// キーボードまたはドラムパッドから再生する
(
MIDIdef.noteOn(\someKeyboard, { arg vel, note;
    Synth("quick", [\freq, note.midicps, \amp, vel.linlin(0, 127, 0, 1)]);
});
)

// パターンを作成し、キーボードから再生します
(
a = Pbind(
    \instrument, "quick",
    \degree, Pwhite(0, 10, 5),
    \amp, Pwhite(0.05, 0.2),
    \dur, 0.1
);
)

// test
a.play;

// pad か keyboard で Trigger pattern 
MIDIdef.noteOn(\quneo, {arg vel, note; a.play});

よくある質問は、持続ノートのノートオンおよびノートオフメッセージの管理方法です。つまり、ADSRエンベロープを使用する場合、キーが押されている限り、各ノートを持続させる必要があります。リリースは、指が対応するキーから外れた場合にのみ行われます(必要に応じて、ADSRエンベロープのセクションをレヴュー)。

そのためには、SuperCollider は単に各キーに対応するシンセノードを追跡する必要があります。次の例に示すように、そのために配列を使用できます。

// A SynthDef with ADSR envelope
SynthDef("quick2", {arg freq = 440, amp = 0.1, gate = 1;
    var snd, env;
    env = Env.adsr(0.01, 0.1, 0.3, 2, amp).kr(2, gate);
    snd = Saw.ar([freq, freq*1.5], env);
    Out.ar(0, snd)
}).add;

// MIDI keyboard でプレイする
(
var noteArray = Array.newClear(128); // 配列にはMIDIノートごとに1つのスロットがあります

MIDIdef.noteOn(\myKeyDown, {arg vel, note;
    noteArray[note] = Synth("quick2", [\freq, note.midicps, \amp, vel.linlin(0,127, 0, 1)]);
    ["NOTE ON", note].postln;
});

MIDIdef.noteOff(\myKeyUp, {arg vel, note;
    noteArray[note].set(\gate, 0);
    ["NOTE OFF", note].postln;
});
)
// PS. SC MIDI接続が行われていることを確認してください(MIDIIn.connectAll)

上記のコードを理解するために:

  • SynthDef "quick2"はADSRエンベロープを使用します。gate 引数は、ノートをオンまたはオフにする役割を果たします。

  • 「noteArray」と呼ばれる配列が作成され、再生されているノートを追跡します。配列のインデックスは、再生中のMIDIノート番号に対応することを意図しています。

  • キーボードでキーが押されるたびに、シンセの再生が開始され(シンセノードがサーバーに作成されます)、そのシンセノードへの参照は配列の一意のスロットに保存されます。配列インデックスは、単にMIDIノート番号そのものです。

  • キーがリリースされるたびに、メッセージ .set(\gate,0) が適切なシンセノードに送信され、ノート番号によって配列から取得されます。

この短いMIDIデモでは、SuperCollider へのMIDIの導入についてのみ説明しました。SuperColliderか らMIDIメッセージを取得するには、MIDIOut ヘルプファイルをご覧ください。

44 OSC

OSC(Open Sound Control)は、ネットワークを介して異なるアプリケーション間または異なるコンピューター間であらゆる種類のメッセージを通信するための優れた方法です。多くの場合、MIDIメッセージよりもはるかに柔軟な代替手段です。ここで詳細に説明するスペースはありませんが、次の例が出発点として適切です。

デモの目標は、OSCメッセージをスマートフォンからコンピューター、またはコンピューターからコンピューターに送信することです。 受信側コンピューターで、次の簡単なコードスニペットを評価します:

(
OSCdef(
key: \whatever,
func: {arg ...args; args.postln},
path: '/stuff')
)

注: [ctrl+.] を押すと OSCdef が中断され、それ以上メッセージを受信しなくなります。

44.1 別のコンピューターから OSC を送信する

これは、両方のコンピューターが SuperCollider を実行し、ネットワークに接続されていることを前提としています。受信側コンピューターのIPアドレスを見つけ、送信側コンピューターで次の行を評価します:

// メッセージを送信するマシンでこれを使用します
~destination = NetAddr("127.0.0.1", 57120); // 宛先の正しいIPアドレスを使用する
computer

~destination.sendMsg("/stuff", "heelloooo");

44.2 スマートフォンから OSC を送信する

  • 無料のOSCアプリを電話にインストールします(たとえば、gyrosc )。
  • 受信側コンピューターのIPアドレスをOSCアプリに(「target」として)入力します。
  • SuperCollider の受信ポートを OSC アプリに入力します(通常57120)。
  • アプリが OSC の送信に使用する正確なメッセージパスを確認し、それに応じて OSCdef を変更します。
  • 電話機がネットワークに接続されていることを確認してください。電話機が適切なパスにメッセージを送信している限り、メッセージがコンピューターに届くのを確認できます。

45 Quarks and plug-ins

他のユーザーが作成したクラスと UGen を追加することにより、SuperCollider の機能を拡張できます。Quark は SuperCollider クラスのパッケージであり、SuperCollider 言語でできることを拡張しています。UGen プラグインは、SuperCollider オーディオ合成サーバーの拡張機能です。

SuperColliderインストールにプラグインと Quark を追加する方法のを知るには、http://supercollider.sourceforge.net/ にアクセスしてください。「Using Quarks」ヘルプファイルも出発点として適切です。http://doc.sccode.org/Guides/UsingQuarks.html

SuperCollider ドキュメントから、Quarks.gui を評価して、使用可能なすべてのクォークのリストを表示できます(新しいウィンドウで開きます)。

46 追加リソース

これで、SuperCollider の紹介は終わりです。いくつかの追加学習リソースを以下にリストします。Enjoy!

Notes


  1. 最初の質問:2番目の Pseq の繰り返し引数として inf の代わりに数値1を使用すると、Pbind は6つのノートが演奏された後(つまり、デュレーション値の完全なシーケンスが実行された後)に停止します。2番目の質問:Pbind を永遠にプレイするには、すべての内部パターンの繰り返し値として inf を使用します。

  2. (a)Pwhite(0, 10) は、0から10までの任意の数を生成します。Prand([0, 4, 1, 5, 9, 10, 2, 3], inf) は、いくつかの数を持つリストからのみ選択します 0から10の間ですが、すべてではありません(6、7、8が存在しないため、このPrandには発生しません)。(b)技術的には、0から100までのすべての数字のリストを提供する場合、Prand を使用できますが、このタスクに Pwhite を使用する方が合理的です:Pwhite(0, 100)。(c)Prand([0, 1, 2, 3], inf) は、リストからアイテムをランダムに選択します。Pwhite(0, 3) は、異なる方法で同じ種類の出力に到達します。0〜3の整数の乱数を生成します。これは、上の Prand と同じオプションのプールになります。ただし、Pwhite(0, 3.0) を記述すると、出力が異なります。Pwhite の入力引数の1つは float(3.0)として書き込まれるため、0.154、1.0、1.45、2.999など、1〜3の浮動小数点数を出力するようになりました。(d)最初の Pbind は32音(8音のシーケンスの4倍)を演奏します。2番目の Pbind は4つのノートのみを再生します:リストから選択された4つのランダムな選択肢( Pseq とは異なり、Prand はリストからすべてのノートを再生する義務を負わないことを思い出してください。3番目と最後の Pbind は、最初と同様に32音を演奏します。

  3. 最初の行:配列 [1, 2, 3, “wow”] は受信オブジェクトです。メッセージは逆です。2行目:文字列「hello」は受信オブジェクトです。dup はメッセージです。4は dup の引数です。3行目:3.1415は受信オブジェクトです。round はメッセージです。0.1は丸めの引数です。4行目:100はレシーバオブジェクトです。

  4. 関数表記のみを使用した書き換え:dup(round(rand(100.0)、0.01),4);

  5. こたえ (a) 24 (b) [5; 5:123] [数字と括弧の両方) © LFSaw ライン全体 (d) Only one e) 0.4 f) 1 と 0.3

  6. SinOsc は、-1〜+1の数値を出力するため、バイポーラです。LFPulse は出力範囲が0-1であるため、ユニポーラです(実際、LFPulse は特に0または1のみを出力し、その間には何もありません)

  7. ソリューション: a = {Out.ar(0, SinOsc.ar(freq: [800, 880], mul: LFPulse.ar([2, 3])))}.play;

  8. (a) 変数 lfn は、単に LFNoise2 を保持します。LFNoise2 の役割は、毎秒(-1から+1まで)新しい乱数を生成し、前の乱数からスライドさせて(すぐに新しい番号にジャンプする LFNoise0 とは異なります)することです。この変数 lfn の最初の使用は、BPFの freq 引数にあります:lfn.range(500, 2500) 。これは、-1から+1までの数値を取り、それらを500〜2500の範囲にスケーリングします。これらの数値は、フィルターの中心周波数として使用されます。これらの周波数は、上下にスライドする音です。最後に、再度 lfn を使用して、panner Pan2 の位置を制御します。数値は既に必要な範囲(-1〜+1)にあるため、( .range メッセージなしで)直接使用されます。これの良い結果は、周波数の変化を位置の変化と結びつけることです。どうやって? LFNoise2 は1秒ごとに新しい乱数に向かってスライドし始め、これがフィルターの周波数とパン位置の同期した変化になります。各場所に2つの異なる LFNoise2 がある場合、変更は無相関になります(これも問題ないかもしれませんが、異なる聴覚の結果です)。(b) mul: of 1は、あまりにもソフトすぎます。フィルターは非常にシャープであるため、元の信号から非常に多くを取り去り、振幅が大幅に低下します。信号を適度に聞こえる範囲に戻す必要があるため、BPFラインの最後に mul: 20 があります。© リズムは、sawのmul: 引数である LFPulse によって駆動されます。LFPulse 周波数(1秒あたりのパルス数)は、1〜10の数値を生成する LFNoise1 によってコントロールされます(それらの間を補間します)。これらの数値は、このパッチの「1秒あたりのノート数」です。