Home記事一覧フォーラム

Synthesijerを使ってFPGA上で動作するゲームを作る

【更新履歴】
2016/09/25 カウンタのバグを修正
2015/11/26 Synthesijer 20151112版に対応
2015/06/16,28 cdc_fifo.vを改良。@ikwzm(twitter)さん、ありがとうございました。
2015/06/13 新規公開

高位合成ツール「Synthesijer」を使ってゲームを開発し、FPGAに実装して動作させます。

ターゲットボードについて

このプロジェクトはFPGA開発ボード「Terasic DE0-CV」「BeMicro CV A9」「BeMicro Max 10」に対応しています。

下準備

プロジェクトのダウンロード、ビルド

Linux上でのビルドを想定しています。
Synthesijerはあらかじめ「高位合成ツール「Synthesijer」を使う」の方法で、また、Quartus Primeは「AlteraのFPGA開発ツール「Quartus Prime」をUbuntuにインストールする」の方法でインストールしているものとします。

ダウンロード:sjr_block.tar.gz

tar xzf sjr_block.tar.gz

sjr_block ディレクトリ以下にファイルが展開されます。sjr_block/synthesijer ディレクトリに移動してmakeします。

cd sjr_block/synthesijer

【Terasic DE0-CV】【BeMicro CV A9】の場合
make

【BeMicro Max 10】の場合
make mini

これでVerilog HDLファイルが生成され、必要なsynthesijerのライブラリファイルがコピーされます。

Quartus Ver.15.0以上 でプロジェクトファイルを開いて「Start Compilation」、「Programmer」で転送して実行します。

プロジェクトファイル:
【Terasic DE0-CV】 sjr_block/de0-cv/de0_cv_start.qpf
【BeMicro CV A9】 sjr_block/bemicro_cva9/bemicro_cva9_start.qpf
【BeMicro Max 10】 sjr_block/bemicro_max10/bemicro_max10_start.qpf

操作方法:KEY1でゲームスタート、KEY0、KEY3で左右移動。

ファイルの解説

ソースコード

これらのソースコードはBSD 2-Clauseライセンスで公開します。

Sjr_Block.java : ブロック崩しゲーム本体
/*
  Copyright (c) 2015, miya
  All rights reserved.

  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/


import synthesijer.rt.*;

public class Sjr_Block
{
  private static final int LINE_WIDTH = 64;
  private static final int STAGE_WIDTH = 32;
  private static final int VRAM_WIDTH = 40;
  private static final int VRAM_HEIGHT = 30;
  private static final byte BALL_COLOR = (byte)255;
  private static final byte WALL_COLOR = (byte)109;
  private static final byte NONE_COLOR = (byte)0;
  private static final int BALL_NUM = 4;
  private static final int BLOCK_NUM = 30;
  private static final int STATE_GAMEOVER = 0;
  private static final int STATE_PLAYING = 1;
  private static final int STATE_CLEAR = 2;
  private static final int BOARD_KEY0 = 1;
  private static final int BOARD_KEY1 = 2;
  private static final int BOARD_KEY2 = 4;
  private static final int BOARD_KEY3 = 8;
  private static final int DEFAULT_GAMESPEED = 4;
  private static final int DEFAULT_PADDLESIZE = 5;
  private static final int MIN_GAMESPEED = 1;
  private static final int MIN_PADDLESIZE = 3;
  private static final int SHOTBALLTIME = 45;
  private int random = -59634649;
  private final VgaVram8 vram = new VgaVram8();
  private final SoundGenerator psg = new SoundGenerator();
  private final HexLED hexLED = new HexLED();
  private final RedLED redLED = new RedLED();
  private final BoardSwitch boardSW = new BoardSwitch();
  private final BoardKey boardKey = new BoardKey();
  // Game
  private final byte[] blockColor = new byte[120];
  private final boolean[] blockEnable = new boolean[120];
  private final boolean[] blockWall = new boolean[120];
  private final int[] ballX = new int[4];
  private final int[] ballY = new int[4];
  private final int[] ballDx = new int[4];
  private final int[] ballDy = new int[4];
  private final boolean[] ballEnable = new boolean[4];
  private int paddleX = 16;
  private int paddleY = 28;
  private int paddleSize;
  private int gameSpeed;
  private int ballCount = 4;
  private int stageState = 0;
  private int ballOnStage = 0;
  private int shotBallTimer = 0;
  private int score = 0;
  private int stageScore = 0;
  private int sleepCount = 0;
  // Sequencer
  private final int[] seqTime = new int[64];
  private final int[] seqDuration = new int[64];
  private final int[] seqChannel = new int[64];
  private final int[] seqNote = new int[64];
  private final int[] seqOctave = new int[64];
  private int seqStartSample;
  private int seqPointer = 0;
  private int seqMusicLength;


  private int rand()
  {
    int r = random;
    r = r ^ (r << 13);
    r = r ^ (r >>> 17);
    r = r ^ (r << 5);
    random = r;
    return r;
  }

  private void resetMusic(int pointer, int length)
  {
    seqStartSample = psg.sample;
    seqPointer = pointer;
    seqMusicLength = length;
  }

  private boolean sequencerRun()
  {
    int seqSample = psg.sample - seqStartSample;

    if (seqSample > seqMusicLength)
    {
      return true;
    }
    else if (seqSample > seqTime[seqPointer])
    {
      // play a note
      psg.setParameter(seqChannel[seqPointer], seqNote[seqPointer], seqOctave[seqPointer], 0, 6144, 6144, seqDuration[seqPointer], 1000);
      psg.play(seqChannel[seqPointer]);
      seqPointer++;
    }
    return false;
  }

  private void setSeqData(int id, int time, int duration, int channel, int note, int octave)
  {
    seqTime[id] = time;
    seqDuration[id] = duration;
    seqChannel[id] = channel;
    seqNote[id] = note;
    seqOctave[id] = octave;
  }

  private void initBall(int id, int x, int y, int dx, int dy)
  {
    ballX[id] = x;
    ballY[id] = y;
    ballDx[id] = dx;
    ballDy[id] = dy;
    ballEnable[id] = false;
  }

  private void paddleRun()
  {
    int newPaddleX = paddleX;
    if ((boardKey.data & BOARD_KEY0) == BOARD_KEY0)
    {
      newPaddleX = paddleX + 2;
      if (newPaddleX + paddleSize > 27)
      {
        newPaddleX = 27 - paddleSize;
      }
      fillRect(paddleX - paddleSize, paddleY, newPaddleX - paddleX, 1, NONE_COLOR);
    }
    else if ((boardKey.data & BOARD_KEY3) == BOARD_KEY3)
    {
      newPaddleX = paddleX - 2;
      if (newPaddleX - paddleSize < 4)
      {
        newPaddleX = 4 + paddleSize;
      }
      fillRect(newPaddleX + paddleSize + 1, paddleY, paddleX - newPaddleX, 1, NONE_COLOR);
    }

    fillRect(newPaddleX - paddleSize, paddleY, (paddleSize << 1) + 1, 1, (byte)255);
    paddleX = newPaddleX;
  }

  private void shotBall()
  {
    if (shotBallTimer > SHOTBALLTIME)
    {
      shotBallTimer = 0;
      if (ballOnStage < BALL_NUM)
      {
        int dx;
        if ((rand() & 1) == 0)
        {
          dx = 1;
        }
        else
        {
          dx = -1;
        }
        initBall(ballOnStage, paddleX + (rand() & 3) - 2, paddleY - 1, dx, -1);
        ballEnable[ballOnStage] = true;
        ballOnStage++;
      }
    }
    else
    {
      shotBallTimer++;
    }
  }

  private boolean blockCollision(int ballID, boolean xNext, boolean yNext)
  {
    int x;
    int y;
    if (xNext)
    {
      x = ballX[ballID] + ballDx[ballID];
    }
    else
    {
      x = ballX[ballID];
    }

    if (yNext)
    {
      y = ballY[ballID] + ballDy[ballID];
    }
    else
    {
      y = ballY[ballID];
    }

    int blockX = x >> 2;
    int blockY = y >> 1;
    int blockID = blockX | (blockY << 3);
    boolean hit;
    if (blockEnable[blockID])
    {
      hit = true;
      psg.play(1);
      if (blockWall[blockID] == false)
      {
        // 衝突したブロックを消す
        blockEnable[blockID] = false;
        drawBlock(blockID);
        score++;
        if (score == BLOCK_NUM)
        {
          stageState = STATE_CLEAR;
        }
      }
    }
    else
    {
      hit = false;
    }
    return hit;
  }

  private void ballRun()
  {
    int i = 0;
    while (i < BALL_NUM)
    {
      if (ballEnable[i] == true)
      {
        boolean hitx = false;
        boolean hity = false;
        // y方向に進めて衝突判定
        if (blockCollision(i, false, true))
        {
          hity = true;
        }
        // x方向に進めて衝突判定
        if (blockCollision(i, true, false))
        {
          hitx = true;
        }
        // x,y方向に進めて衝突判定
        if ((!hitx) && (!hity) && (blockCollision(i, true, true)))
        {
          hitx = true;
          hity = true;
        }

        if (hitx)
        {
          int dxtmp = 0 - ballDx[i];
          ballDx[i] = dxtmp;
        }

        if (hity)
        {
          int dytmp = 0 - ballDy[i];
          ballDy[i] = dytmp;
        }

        int nx = ballX[i] + ballDx[i];
        int ny = ballY[i] + ballDy[i];
        // paddle collision
        if ((ny == paddleY) && (nx >= paddleX - paddleSize) && (nx <= paddleX + paddleSize))
        {
          psg.play(2);
          int dytmp = 0 - ballDy[i];
          ballDy[i] = dytmp;
          ny = ballY[i] + ballDy[i];
        }

        vram.data[nx + (ny << 6 /*LINE_WIDTH_IN_BITS*/)] = BALL_COLOR;
        vram.data[ballX[i] + (ballY[i] << 6 /*LINE_WIDTH_IN_BITS*/)] = NONE_COLOR;
        ballX[i] = nx;
        ballY[i] = ny;

        // out
        if (ny > paddleY)
        {
          psg.play(3);
          ballEnable[i] = false;
          ballCount--;
          if (ballCount == 0)
          {
            stageState = STATE_GAMEOVER;
          }
        }
      }
      i++;
    }
  }

  private void fillRect(int x, int y, int width, int height, byte color)
  {
    int iy = y << 6 /*LINE_WIDTH_IN_BITS*/;
    int iye = (y + height) << 6 /*LINE_WIDTH_IN_BITS*/;
    int ixe = x + width;
    while (iy < iye)
    {
      int ix = x;
      while (ix < ixe)
      {
        vram.data[ix + iy] = color;
        ix++;
      }
      iy += LINE_WIDTH;
    }
  }

  private void drawBlock(int id)
  {
    byte color;
    if (blockEnable[id] == true)
    {
      color = blockColor[id];
    }
    else
    {
      color = NONE_COLOR;
    }
    int x = id & 7;
    int y = id >>> 3;
    fillRect(x << 2, y << 1, 4, 2, color);
  }

  private void init()
  {
    psg.start();
    vram.offset_h = 0;
    vram.offset_v = 0;

    // music data
    // メインBGM
    setSeqData( 0,      0,  9000, 0,  0, 4);
    setSeqData( 1,  10000,  9000, 0,  2, 4);
    setSeqData( 2,  20000,  9000, 0,  4, 4);
    setSeqData( 3,  30000,  9000, 0,  5, 4);
    setSeqData( 4,  40000, 19000, 0,  7, 4);
    setSeqData( 5,  60000, 19000, 0,  7, 4);
    setSeqData( 6,  80000,  9000, 0,  7, 4);
    setSeqData( 7,  90000,  9000, 0,  5, 4);
    setSeqData( 8, 100000,  9000, 0,  4, 4);
    setSeqData( 9, 110000,  9000, 0,  2, 4);
    setSeqData(10, 120000, 29000, 0,  4, 4);
    setSeqData(11, 160000,  9000, 0,  0, 4);
    setSeqData(12, 170000,  9000, 0,  2, 4);
    setSeqData(13, 180000,  9000, 0,  4, 4);
    setSeqData(14, 190000,  9000, 0,  5, 4);
    setSeqData(15, 200000, 19000, 0,  7, 4);
    setSeqData(16, 220000, 19000, 0,  7, 4);
    setSeqData(17, 240000,  9000, 0,  7, 4);
    setSeqData(18, 250000,  9000, 0,  5, 4);
    setSeqData(19, 260000,  9000, 0,  4, 4);
    setSeqData(20, 270000,  9000, 0,  2, 4);
    setSeqData(21, 280000, 29000, 0,  0, 4);
    setSeqData(22, 320000,  9000, 0,  0, 5);
    setSeqData(23, 330000,  9000, 0, 11, 4);
    setSeqData(24, 340000,  9000, 0,  9, 4);
    setSeqData(25, 350000,  9000, 0,  7, 4);
    setSeqData(26, 360000,  9000, 0,  0, 5);
    setSeqData(27, 370000,  9000, 0, 11, 4);
    setSeqData(28, 380000,  9000, 0,  9, 4);
    setSeqData(29, 390000,  9000, 0,  7, 4);
    setSeqData(30, 400000,  9000, 0,  5, 4);
    setSeqData(31, 410000,  9000, 0,  4, 4);
    setSeqData(32, 420000,  9000, 0,  2, 4);
    setSeqData(33, 430000,  9000, 0,  0, 4);
    setSeqData(34, 440000, 29000, 0,  7, 4);
    setSeqData(35, 480000,  9000, 0,  0, 5);
    setSeqData(36, 490000,  9000, 0, 11, 4);
    setSeqData(37, 500000,  9000, 0,  9, 4);
    setSeqData(38, 510000,  9000, 0,  7, 4);
    setSeqData(39, 520000,  9000, 0,  0, 5);
    setSeqData(40, 530000,  9000, 0, 11, 4);
    setSeqData(41, 540000,  9000, 0,  9, 4);
    setSeqData(42, 550000,  9000, 0,  7, 4);
    setSeqData(43, 560000,  9000, 0,  5, 4);
    setSeqData(44, 570000,  9000, 0,  4, 4);
    setSeqData(45, 580000,  9000, 0,  2, 4);
    setSeqData(46, 590000,  9000, 0,  7, 4);
    setSeqData(47, 600000, 29000, 0,  0, 4);
    setSeqData(48, 999999,     0, 0,  0, 4);
    // クリアファンファーレ
    setSeqData(49,      0,     0, 0,  0, 4);
    setSeqData(50,  40000,  9000, 0,  0, 4);
    setSeqData(51,  50000,  9000, 0,  2, 4);
    setSeqData(52,  60000,  9000, 0,  4, 4);
    setSeqData(53,  70000,  9000, 0,  5, 4);
    setSeqData(54,  80000,  9000, 0,  7, 4);
    setSeqData(55,  90000,  9000, 0,  9, 4);
    setSeqData(56, 100000,  9000, 0, 11, 4);
    setSeqData(57, 110000,  9000, 0,  7, 4);
    setSeqData(58, 120000, 19000, 0,  0, 5);
    setSeqData(59, 140000, 19000, 0,  0, 5);
    setSeqData(60, 160000, 19000, 0,  0, 5);
    setSeqData(61, 999999,     0, 0,  0, 5);
    // sound effect settings
    psg.setParameter(1, 0, 6, -8000, 4096, 8192, 100010, 100000);
    psg.setParameter(2, 0, 2, 600, 6144, 6144, 30010, 30000);
    psg.setParameter(3, 0, 4, -100, 8192, 4096, 96000, 10000);

    // clear screen
    fillRect(0, 0, VRAM_WIDTH, VRAM_HEIGHT, WALL_COLOR);
  }

  private void gameStart()
  {
    // clear stage screen
    fillRect(0, 0, 32, VRAM_HEIGHT, (byte)0);

    // init blocks
    for (int y = 0; y < 15; y++)
    {
      for (int x = 0; x < 8; x++)
      {
        int bid = x | (y << 3);
        if ((x == 0) || (x == 7) || (y == 0))
        {
          blockEnable[bid] = true;
          blockColor[bid] = WALL_COLOR;
          blockWall[bid] = true;
        }
        else
        {
          blockColor[bid] = (byte)(128 + bid);
          blockWall[bid] = false;
          if ((y > 2) && (y < 8))
          {
            blockEnable[bid] = true;
          }
          else
          {
            blockEnable[bid] = false;
          }
        }
        drawBlock(bid);
      }
    }

    // init balls
    for (int i = 0; i < 4; i++)
    {
      initBall(i, 19, 26, -1, -1);
    }

    shotBallTimer = 0;
    ballOnStage = 0;
    ballCount = BALL_NUM;
    score = 0;
    paddleX = 16;
    sleepCount = 0;

    // init sequencer
    resetMusic(0, 640000);

    stageState = STATE_PLAYING;
  }

  private void stageClear()
  {
    stageScore++;
    // draw stage score
    if (stageScore <= 30)
    {
      fillRect(32, 30 - stageScore, 8, 1, (byte)(stageScore + 225));
    }
    resetMusic(49, 240000);
    while (!sequencerRun())
    {
    }
    paddleSize--;
    if (paddleSize < MIN_PADDLESIZE)
    {
      paddleSize = DEFAULT_PADDLESIZE;
      gameSpeed--;
    }
    if (gameSpeed < MIN_GAMESPEED)
    {
      gameSpeed = MIN_GAMESPEED;
    }
    gameStart();
  }

  @auto
  public void main()
  {
    init();

    while (true)
    {
      // vsync wait
      while (vram.vsync == true)
      {
      }
      while (vram.vsync == false)
      {
      }

      if (stageState == STATE_GAMEOVER)
      {
        gameSpeed = DEFAULT_GAMESPEED;
        paddleSize = DEFAULT_PADDLESIZE;
        rand();
        if ((boardKey.data & BOARD_KEY1) == BOARD_KEY1)
        {
          // clear score
          stageScore = 0;
          fillRect(32, 0, 8, VRAM_HEIGHT, (byte)0);
          gameStart();
        }
      }
      else if (stageState == STATE_PLAYING)
      {
        // シーケンサを実行して曲が終わったら曲の最初からリピートする
        if (sequencerRun())
        {
          resetMusic(0, 640000);
        }

        if (sleepCount == 0)
        {
          paddleRun();
          ballRun();
          shotBall();
        }
      }
      else if (stageState == STATE_CLEAR)
      {
        stageClear();
      }

      redLED.data = boardSW.data;
      hexLED.data = stageScore;
      sleepCount++;
      if (sleepCount == gameSpeed)
      {
        sleepCount = 0;
      }
    }
  }
}

SoundGenerator.java : サウンドジェネレータ(PSG)
/*
  Copyright (c) 2015, miya
  All rights reserved.

  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/



public class SoundGenerator extends Thread
{
  public int sample = 0;
  private static final int CHANNELS = 4;
  private static final int ZERO = 0;
  private final AudioOutput audio = new AudioOutput();
  // 以下のフィールドは親スレッドから書き換えられるので run() 内ではread only
  private final int[] freq = new int[4];
  private final int[] pitchBend = new int[4];
  private final int[] vol_R = new int[4];
  private final int[] vol_L = new int[4];
  private final int[] gateTime = new int[4];
  private final int[] decay = new int[4];
  // 以下のフィールドは run() 内で読み書き可
  private final int[] localFreq = new int[4];
  private final int[] localVol_R = new int[4];
  private final int[] localVol_L = new int[4];
  private final int[] localGateTime = new int[4];
  private final int[] localDecay = new int[4];
  private final int[] localFreqCounter = new int[4];
  private final int[] noteData = new int[12];
  // 両方から読み書きするフィールド
  private final boolean[] playFlag = new boolean[4];
  private final boolean[] stopFlag = new boolean[4];

  public void setParameter(int channel, int note_in, int octave_in, int pitchBend_in, int vol_R_in, int vol_L_in, int gateTime_in, int decay_in)
  {
    freq[channel] = noteData[note_in] >>> (8 - octave_in);
    pitchBend[channel] = pitchBend_in;
    vol_R[channel] = vol_R_in;
    vol_L[channel] = vol_L_in;
    gateTime[channel] = gateTime_in;
    decay[channel] = decay_in;
  }

  public void play(int channel)
  {
    playFlag[channel] = true;
  }

  public void stop(int channel)
  {
    stopFlag[channel] = true;
  }

  public void run()
  {
    // 平均律:pow(2, (x/12)): (x: 0-11)
    // round(pow(2,(note/12.0)) * 440.0 * octave * 0x100000000 / 48000) : (note: 3-14)
    noteData[0] = 374557749;  // C8
    noteData[1] = 396830112;  // C#8
    noteData[2] = 420426858;  // D8
    noteData[3] = 445426740;  // D#8
    noteData[4] = 471913192;  // E8
    noteData[5] = 499974611;  // F8
    noteData[6] = 529704648;  // F#8
    noteData[7] = 561202526;  // G8
    noteData[8] = 594573365;  // G#8
    noteData[9] = 629928537;  // A8
    noteData[10] = 667386037; // A#8
    noteData[11] = 707070876; // B8
    boolean validToggle = false;

    while (true)
    {
      int mixer_R = 0;
      int mixer_L = 0;
      int i = 0;
      while (i < CHANNELS)
      {
        if (playFlag[i])
        {
          playFlag[i] = false;
          localFreq[i] = freq[i];
          localFreqCounter[i] = 0;
          localGateTime[i] = 0;
          localDecay[i] = 0;
          localVol_R[i] = vol_R[i];
          localVol_L[i] = vol_L[i];
        }

        if (stopFlag[i])
        {
          stopFlag[i] = false;
          localVol_R[i] = 0;
          localVol_L[i] = 0;
        }

        // localFreqCounterの負号で矩形波を作る(1:+ 0:-)
        // localFreq = f(Hz) * 0x100000000 / 48000
        localFreqCounter[i] += localFreq[i];
        localFreq[i] += pitchBend[i];
        // localDecay:固定小数点 15bit.16bit
        localDecay[i] += decay[i];
        int d = (localDecay[i] >>> 8) >>> 8;
        localDecay[i] = localDecay[i] & 0xffff;
        // ボリュームを減衰させる
        if (localVol_R[i] > d)
        {
          localVol_R[i] -= d;
        }
        else
        {
          localVol_R[i] = 0;
        }

        if (localVol_L[i] > d)
        {
          localVol_L[i] -= d;
        }
        else
        {
          localVol_L[i] = 0;
        }

        // ゲートタイムを超えたらボリュームを0に
        localGateTime[i] = localGateTime[i] + 1;
        if (localGateTime[i] > gateTime[i])
        {
          localVol_R[i] = 0;
          localVol_L[i] = 0;
        }

        // 各チャンネルの出力をmix
        if (localFreqCounter[i] >= ZERO)
        {
          mixer_R += localVol_R[i];
          mixer_L += localVol_L[i];
        }
        else
        {
          mixer_R -= localVol_R[i];
          mixer_L -= localVol_L[i];
        }
        i++;
      }

      // サンプリング値をunsigned 16bitに変換してR+Lを32bitにpack
      int mixer_R2 = (mixer_R + 0x8000) & 0xffff;
      int mixer_L2 = (mixer_L + 0x8000) & 0xffff;
      int data = (mixer_R2 << 16) | mixer_L2;
      while (audio.full == true)
      {
        yield();
      }
      validToggle = !validToggle;
      audio.data = data;
      // dataを書いた後にvalid_toggleフラグをトグル(true<->false)する
      // (次のデータが用意できたことを知らせる)
      audio.valid_toggle = validToggle;
      sample++;
    }
  }
}

AudioOutput.java : 音声出力ライブラリ Java側プログラム
/*
  Copyright (c) 2015, miya
  All rights reserved.

  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/


import java.util.EnumSet;

import synthesijer.hdl.HDLModule;
import synthesijer.hdl.HDLPort;
import synthesijer.hdl.HDLPort.DIR;
import synthesijer.hdl.HDLPrimitiveType;

public class AudioOutput extends HDLModule
{
  int data;
  boolean valid_toggle;
  boolean full;

  public AudioOutput(String... args)
  {
    super("audio_output", "clk", "reset");

    newPort("data", DIR.IN, HDLPrimitiveType.genSignedType(32));
    newPort("valid_toggle", DIR.IN, HDLPrimitiveType.genBitType());
    newPort("full", DIR.OUT, HDLPrimitiveType.genBitType());

    newPort("ext_clka", DIR.IN, HDLPrimitiveType.genBitType(), EnumSet.of(HDLPort.OPTION.EXPORT));
    newPort("ext_reseta", DIR.IN, HDLPrimitiveType.genBitType(), EnumSet.of(HDLPort.OPTION.EXPORT));
    newPort("ext_audio_r", DIR.OUT, HDLPrimitiveType.genBitType(), EnumSet.of(HDLPort.OPTION.EXPORT));
    newPort("ext_audio_l", DIR.OUT, HDLPrimitiveType.genBitType(), EnumSet.of(HDLPort.OPTION.EXPORT));
  }
}

audio_output.v : 音声出力ライブラリ Verilog HDL側コード
/*
  Copyright (c) 2015, miya
  All rights reserved.

  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/


module audio_output
  (
   input        clk,
   input        reset,
   input [31:0] data,
   input        valid_toggle,
   output       full,
   input        ext_clka,
   input        ext_reseta,
   output       ext_audio_r,
   output       ext_audio_l
   );

  // data read frequency: 18MHz / 48000Hz = 375 (-1)
  parameter READ_FREQ = 374;
  parameter FIFO_DEPTH_IN_BITS = 3;

  // data write
  reg           toggle;
  reg           toggle_prev;
  reg [31:0]    data_in;
  wire          req_cw;

  assign req_cw = ((toggle_prev != toggle) && (full == 1'b0)) ? 1'b1 : 1'b0;

  always @(posedge clk)
    begin
      if (reset == 1'b1)
        begin
          toggle <= 1'b0;
          data_in <= 1'd0;
          toggle_prev <= 1'b0;
        end
      else
        begin
          toggle <= valid_toggle;
          data_in <= data;
          toggle_prev <= toggle;
        end
    end

  // data read
  wire          req_ca;
  wire          empty_ca;
  reg [15+1:0]  sample_sum_r_ca;
  reg [15+1:0]  sample_sum_l_ca;
  wire [31:0]   data_out_ca;
  reg [31:0]    data_out_reg_ca;
  reg           data_valid_ca;
  reg [8:0]     read_freq_counter;

  assign req_ca = ((empty_ca == 1'b0) && (read_freq_counter == 9'd0)) ? 1'b1 : 1'b0;
  assign ext_audio_r = sample_sum_r_ca[15+1];
  assign ext_audio_l = sample_sum_l_ca[15+1];

  always @(posedge ext_clka)
    begin
      if (ext_reseta == 1'b1)
        begin
          sample_sum_r_ca <= 1'd0;
          sample_sum_l_ca <= 1'd0;
          data_valid_ca <= 1'b0;
          data_out_reg_ca <=  1'd0;
          read_freq_counter <= 1'd0;
        end
      else
        begin
          // read data delays 1 cycle
          data_valid_ca <= req_ca;
          if (data_valid_ca)
            begin
              data_out_reg_ca <= data_out_ca;
            end
          // delta sigma
          sample_sum_r_ca <= sample_sum_r_ca[15:0] + data_out_reg_ca[31:16];
          sample_sum_l_ca <= sample_sum_l_ca[15:0] + data_out_reg_ca[15:0];
          // read data once per READ_FREQ+1 cycles
          if (read_freq_counter == 9'd0)
            begin
              read_freq_counter <= READ_FREQ;
            end
          else
            begin
              read_freq_counter <= read_freq_counter - 1'd1;
            end
        end
    end

  cdc_fifo
    #(
      .DATA_WIDTH (32),
      .ADDR_WIDTH (FIFO_DEPTH_IN_BITS)
      )
  cdc_fifo_0
    (
     .clk_cr (ext_clka),
     .data_cr (data_out_ca),
     .req_cr (req_ca),
     .empty_cr (empty_ca),
     .clk_cw (clk),
     .data_cw (data_in),
     .req_cw (req_cw),
     .full_cw (full)
     );

endmodule

cdc_fifo.v : デュアルクロックFIFO
/*
  Copyright (c) 2015, miya
  All rights reserved.

  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/


module cdc_fifo
  #(
    parameter DATA_WIDTH = 8,
    parameter ADDR_WIDTH = 8
    )
  (
   // -------- clock domain: read  --------
   input                   clk_cr,
   output [DATA_WIDTH-1:0] data_cr,
   input                   req_cr,
   output                  empty_cr,
   // -------- clock domain: write --------
   input                   clk_cw,
   input [DATA_WIDTH-1:0]  data_cw,
   input                   req_cw,
   output                  full_cw
   );

  // binary to gray-code conversion
  function [DATA_WIDTH-1:0] bin2gray
    (
     input [DATA_WIDTH-1:0] data_in
     );
    begin
      bin2gray = {1'b0, data_in[DATA_WIDTH-1:1]} ^ data_in[DATA_WIDTH-1:0];
    end
  endfunction

  // -------- clock domain: read  --------
  reg  [ADDR_WIDTH-1:0] addr_r_cr = 1'd0;
  reg [ADDR_WIDTH-1:0]  addr_r_gray_cr = 1'd0;
  wire [ADDR_WIDTH-1:0] addr_r_next_cr;
  wire [ADDR_WIDTH-1:0] addr_w_sync_cr;
  assign addr_r_next_cr = addr_r_cr + 1'd1;
  assign empty_cr = (addr_r_gray_cr == addr_w_sync_cr) ? 1'b1 : 1'b0;

  always @(posedge clk_cr)
    begin
      if ((req_cr == 1'b1) && (empty_cr == 1'b0))
        begin
          addr_r_cr <= addr_r_next_cr;
          addr_r_gray_cr <= bin2gray(addr_r_next_cr);
        end
    end

  // -------- clock domain: write --------
  reg  [ADDR_WIDTH-1:0] addr_w_cw = 1'd0;
  wire [ADDR_WIDTH-1:0] addr_w_next_gray_cw;
  wire [ADDR_WIDTH-1:0] addr_r_gray_cw;
  wire                  we_cw;
  wire [ADDR_WIDTH-1:0] addr_w_next_cw;
  wire [ADDR_WIDTH-1:0] addr_r_sync_cw;
  reg [ADDR_WIDTH-1:0]  addr_w_gray_cw = 1'd0;
  assign addr_w_next_cw = addr_w_cw + 1'd1;
  assign full_cw = (addr_w_next_gray_cw == addr_r_sync_cw) ? 1'b1 : 1'b0;
  assign we_cw = ((req_cw == 1'b1) && (full_cw == 1'b0)) ? 1'b1 : 1'b0;
  assign addr_w_next_gray_cw = bin2gray(addr_w_next_cw);

  always @(posedge clk_cw)
    begin
      if (we_cw == 1'b1)
        begin
          addr_w_cw <= addr_w_next_cw;
          addr_w_gray_cw <= addr_w_next_gray_cw;
        end
    end

  synchronizer
    #(
      .DATA_WIDTH (ADDR_WIDTH)
      )
  synchronizer_0
    (
     .clk_out (clk_cw),
     .data_in (addr_r_gray_cr),
     .data_out (addr_r_sync_cw)
     );

  synchronizer
    #(
      .DATA_WIDTH (ADDR_WIDTH)
      )
  synchronizer_1
    (
     .clk_out (clk_cr),
     .data_in (addr_w_gray_cw),
     .data_out (addr_w_sync_cr)
     );

  dual_port_ram
    #(
      .DATA_WIDTH (DATA_WIDTH),
      .ADDR_WIDTH (ADDR_WIDTH)
      )
  dual_port_ram_0
    (
     .data_in (data_cw),
     .read_addr (addr_r_cr),
     .write_addr (addr_w_cw),
     .we (we_cw),
     .read_clock (clk_cr),
     .write_clock (clk_cw),
     .data_out (data_cr)
     );

endmodule

synchronizer.v : 同期化回路
/*
  Copyright (c) 2015, miya
  All rights reserved.

  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/


module synchronizer
  #(
    parameter DATA_WIDTH=8
    )
  (
   input                     clk_out,
   input [(DATA_WIDTH-1):0]  data_in,
   output [(DATA_WIDTH-1):0] data_out
   );

  reg [(DATA_WIDTH-1):0]     data_out_d1;
  reg [(DATA_WIDTH-1):0]     data_out_d2;

  always @(posedge clk_out)
    begin
      data_out_d2 <= data_out_d1;
      data_out_d1 <= data_in;
    end

  assign data_out = data_out_d2;

endmodule

cdc_synchronizer.v : 同期化回路
/*
  Copyright (c) 2015-2016, miya
  All rights reserved.

  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/


// latency: 2 clk_in cycles + 3 clk_out cycles
// data_in value must be held for 4 clk_out cycles
module cdc_synchronizer
  #(
    parameter DATA_WIDTH=8
    )
  (
   input                     clk_in,
   input                     clk_out,
   input [(DATA_WIDTH-1):0]  data_in,
   output [(DATA_WIDTH-1):0] data_out,
   input                     reset_in
   );

  reg [(DATA_WIDTH-1):0]     data_in_reg;
  reg [(DATA_WIDTH-1):0]     data_out_reg[2:0];
  reg                        change_flag_in;
  reg [2:0]                  change_flag_out;

  always @(posedge clk_in)
    begin
      if (reset_in == 1'b1)
        begin
          change_flag_in <= 1'b0;
        end
      else if (data_in_reg != data_in)
        begin
          change_flag_in <= ~change_flag_in;
        end
      data_in_reg <= data_in;
    end

  always @(posedge clk_out)
    begin
      if (change_flag_out[2] == change_flag_out[1])
        begin
          data_out_reg[2] <= data_out_reg[1];
        end
    end

  always @(posedge clk_out)
    begin
      change_flag_out <= {change_flag_out[1:0], change_flag_in};
      data_out_reg[1] <= data_out_reg[0];
      data_out_reg[0] <= data_in_reg;
    end

  assign data_out = data_out_reg[2];
endmodule

参考文献

Virtex Synthesizable Delta-Sigma DAC (Xilinx: pdf)

PWM, sigma-delta and one-bit DAC (fpga4fun.com)