Annotation of nono/vm/sound.cpp, revision 1.1.1.2

1.1       root        1: //
                      2: // nono
                      3: // Copyright (C) 2025 nono project
                      4: // Licensed under nono-license.txt
                      5: //
                      6: 
                      7: //
                      8: // サウンドレンダラ
                      9: //
                     10: 
1.1.1.2 ! root       11: // vtime / BLK_TSEC をブロック時刻と呼ぶことにする。
1.1       root       12: //
                     13: // 各サウンドトラックは 2 (=NSRCBLKS) ブロック分のバッファを持つ。
                     14: // ブロック境界の 000'000_nsec ちょうどにソースデバイスとサウンドレンダラの
                     15: // イベントが重なってどちらの順で実行しても構わないようにするため。
                     16: //
                     17: // サウンドレンダラのソース合成バッファは 1ブロックでいい (NMIXBLKS)。
                     18: //
                     19: // サウンドレンダラからホストドライバへの出力リングバッファは
                     20: // 4 (=NOUTBLKS) ブロック。少なくとも 3 あればいいのできりをよくする。
                     21: 
                     22: #include "sound.h"
1.1.1.2 ! root       23: #include "bitmap.h"
1.1       root       24: #include "event.h"
                     25: #include "hostsound.h"
                     26: #include "mainapp.h"
                     27: #include "monitor.h"
                     28: #include "scheduler.h"
                     29: #include "syncer.h"
                     30: #include <algorithm>
                     31: #include <cmath>
                     32: 
                     33: #define SAMPLES_TO_BYTES(samples)      ((samples) * sizeof(int16))
                     34: #define SAMPLES_TO_FRAMES(samples)     ((samples) / NCHAN)
                     35: #define FRAMES_TO_SAMPLES(frames)      ((frames) * NCHAN)
                     36: 
                     37: //
                     38: // サウンドトラック
                     39: //
                     40: 
                     41: // コンストラクタ
                     42: SoundTrack::SoundTrack(Device *dev_, size_t mixer_samples_per_blk)
                     43: {
                     44:        dev = dev_;
                     45:        samples_per_blk = mixer_samples_per_blk;
                     46: 
                     47:        // 例外はこれをコンストラクトするところで catch してある。
                     48:        buf.reset(new int16[samples_per_blk * NSRCBLKS]);
                     49: 
                     50:        std::fill(blkseq.begin(), blkseq.end(), (uint64)-1);
                     51:        SoundRenderer::ClearDBFS(dbfs);
                     52: }
                     53: 
                     54: // バッファの idx 番目のブロック先頭を返す。
                     55: // 時刻や使用状況によらない。
                     56: int16 *
                     57: SoundTrack::GetBlockByIdx(uint32 idx)
                     58: {
                     59:        assert(idx < NSRCBLKS);
                     60:        auto p = buf.get() + idx * samples_per_blk;
                     61:        return p;
                     62: }
                     63: 
                     64: // ブロック時刻 vseq のブロック先頭を返す。有効でなければ NULL を返す。
                     65: int16 *
                     66: SoundTrack::GetBlockBySeq(uint64 vseq)
                     67: {
                     68:        uint32 idx = vseq % NSRCBLKS;
                     69: 
                     70:        if (blkseq[idx] == vseq) {
                     71:                return GetBlockByIdx(idx);
                     72:        }
                     73:        return NULL;
                     74: }
                     75: 
                     76: // 時刻 vseq をブロックと紐付けて (すでに紐付いていれば何もしない)、
                     77: // このブロックの先頭を返す。
                     78: int16 *
                     79: SoundTrack::AllocSeq(uint64 vseq)
                     80: {
                     81:        uint32 idx = vseq % NSRCBLKS;
                     82: 
                     83:        if (__predict_true(blkseq[idx] == (uint64)-1)) {
                     84:                // バッファは消去済み。
                     85:                blkseq[idx] = vseq;
                     86:        } else if (blkseq[idx] != vseq) {
                     87:                // バッファが未消去なので書きつぶす。(通常起きないはず)
                     88:                ClearSeq(vseq);
                     89:                blkseq[idx] = vseq;
                     90:        }
                     91: 
                     92:        return GetBlockByIdx(idx);
                     93: }
                     94: 
                     95: // 時刻 vseq のブロックの紐付けをクリアする。
                     96: // (サウンドスレッドから呼ばれるはず)
                     97: void
                     98: SoundTrack::ClearSeq(uint64 vseq)
                     99: {
                    100:        uint32 idx = vseq % NSRCBLKS;
                    101: 
                    102:        int16 *blk = GetBlockByIdx(idx);
                    103:        memset(blk, 0, SAMPLES_TO_BYTES(samples_per_blk));
                    104:        blkseq[idx] = (int64)-1;
                    105: }
                    106: 
                    107: 
                    108: //
                    109: // サウンドレンダラ
                    110: //
                    111: 
                    112: // コンストラクタ
                    113: SoundRenderer::SoundRenderer()
                    114:        : inherited(OBJ_SOUND_RENDERER)
                    115: {
                    116:        // ミキサー周波数は機種ごとに違う。
                    117:        if (gMainApp.IsX68030()) {
                    118:                // X68030 では 62.5kHz を使う。(adpcm.cpp 参照)
                    119:                mixerfreq = 62500;
                    120:        } else {
                    121:                assert(false);
                    122:        }
                    123: 
                    124:        ClearDBFS(master_dbfs);
                    125: 
                    126:        monitor = gMonitorManager->Regist(ID_MONITOR_SOUND, this);
1.1.1.2 ! root      127:        monitor->SetCallback(&SoundRenderer::MonitorScreen,
        !           128:                             &SoundRenderer::MonitorBitmap);
1.1       root      129:        monitor->SetSize(40, 2);
                    130: }
                    131: 
                    132: // デストラクタ
                    133: SoundRenderer::~SoundRenderer()
                    134: {
                    135:        TerminateThread();
                    136: }
                    137: 
                    138: bool
                    139: SoundRenderer::Create()
                    140: {
                    141:        try {
                    142:                hostsnd.reset(new HostSoundDevice(this));
                    143:        } catch (...) { }
                    144:        if ((bool)hostsnd == false) {
                    145:                warnx("Failed to initialize HostSoundDevice at %s", __method__);
                    146:                return false;
                    147:        }
                    148: 
                    149:        return true;
                    150: }
                    151: 
                    152: void
                    153: SoundRenderer::SetLogLevel(int loglevel_)
                    154: {
                    155:        inherited::SetLogLevel(loglevel_);
                    156: 
                    157:        if ((bool)hostsnd) {
                    158:                hostsnd->SetLogLevel(loglevel_);
                    159:        }
                    160: }
                    161: 
                    162: // 初期化
                    163: bool
                    164: SoundRenderer::Init()
                    165: {
                    166:        syncer = GetSyncer();
                    167: 
                    168:        // トラック合成用バッファ。(1ブロック分)
1.1.1.2 ! root      169:        auto mixer_frames_per_blk = (uint64)mixerfreq * BLK_TSEC / 1_sec;
1.1       root      170:        mixer_samples_per_blk = FRAMES_TO_SAMPLES(mixer_frames_per_blk);
                    171:        mixbuf.reset(new(std::nothrow) int16[mixer_samples_per_blk]);
                    172:        if (mixbuf == NULL) {
1.1.1.2 ! root      173:                warnx("Could not allocate %zu bytes at %s",
1.1       root      174:                        SAMPLES_TO_BYTES(mixer_samples_per_blk), __method__);
                    175:                return false;
                    176:        }
                    177: 
                    178:        // 出力バッファなど実行時にも変わるものを設定。
                    179:        request_outfreq = mixerfreq;    // XXX 周波数変換はまだ
                    180:        if (Reconfig(true) == false) {
1.1.1.2 ! root      181:                // エラーメッセージは表示済み。
1.1       root      182:                return false;
                    183:        }
                    184: 
                    185:        auto evman = GetEventManager();
                    186:        play_event = evman->Regist(this,
                    187:                ToEventCallback(&SoundRenderer::Callback),
                    188:                "Sound Renderer");
                    189: 
                    190:        return true;
                    191: }
                    192: 
                    193: // リセット
                    194: void
                    195: SoundRenderer::ResetHard(bool poweron)
                    196: {
                    197:        // 各デバイスがリセット時に Stop*() を呼ぶので
                    198:        // 自動的にこっちも停止するはず。
                    199: }
                    200: 
                    201: // 出力周波数かホストドライバの変更 (サウンドスレッドで呼ばれる)。
                    202: // エラーが起きた場合、
                    203: // 起動時なら、warn() 等で表示して false を返す(終了する)。
                    204: // 実行中なら、warn() 等で表示してもいいが、必ず None にフォールバックして
                    205: // true を返すこと。
                    206: bool
                    207: SoundRenderer::Reconfig(bool startup)
                    208: {
                    209:        assert(hostsnd->IsQEmpty());
                    210: 
                    211:        // 出力周波数が変わったら出力リングバッファをリサイズ。
                    212:        // outbuf は NOUTBLKS ブロック分。
                    213:        bool need_resize = false;
                    214:        if (outfreq != request_outfreq) {
                    215:                outfreq  = request_outfreq;
                    216:                need_resize = true;
                    217:        }
                    218: 
1.1.1.2 ! root      219:        auto out_frames_per_blk = (uint64)outfreq * BLK_TSEC / 1_sec;
1.1       root      220:        out_samples_per_blk = FRAMES_TO_SAMPLES(out_frames_per_blk);
                    221: 
                    222:        if (need_resize) {
                    223:                auto samples = out_samples_per_blk * NOUTBLKS;
                    224:                outbuf.reset(new(std::nothrow) int16[samples]);
                    225:                if (outbuf == NULL) {
1.1.1.2 ! root      226:                        warnx("Could not allocate %zu bytes at %s",
1.1       root      227:                                SAMPLES_TO_BYTES(samples), __method__);
                    228:                        return false;
                    229:                }
                    230:        }
                    231: 
                    232:        // ホストドライバも必ず再切替になる。
                    233:        if (hostsnd->NotifyReselectDriver() == false) {
1.1.1.2 ! root      234:                warn("%s: NotifyReselectDriver failed?", __method__);
1.1       root      235:                return false;
                    236:        }
                    237: 
                    238:        return true;
                    239: }
                    240: 
                    241: // サウンドトラック(ソースデバイス)を登録する。
                    242: // 登録された SoundTrack へのポインタを返す。
                    243: // 各ソースデバイスが Init() で呼ぶ。
                    244: // Unregist 操作はない。
                    245: SoundTrack *
                    246: SoundRenderer::RegistTrack(Device *dev)
                    247: {
                    248:        assert(tracks.size() < MAX_SOUND_SOURCES);
                    249: 
                    250:        try {
                    251:                // この emplace_back が new SoundTrack(..)。
                    252:                tracks.emplace_back(dev, mixer_samples_per_blk);
                    253:        } catch (...) {
                    254:                warnx("Failed to initialize SoundTrack");
                    255:                return NULL;
                    256:        }
                    257: 
                    258:        monitor->AddHeight(3);
                    259: 
                    260:        return &tracks.back();
                    261: }
                    262: 
                    263: // 再生を開始する。
                    264: // 各サウンドデバイスが呼ぶ。
                    265: void
                    266: SoundRenderer::StartPlay(SoundTrack *track)
                    267: {
                    268:        if (track->status != Running) {
                    269:                putlog(1, "Playing [%s]", track->GetName().c_str());
                    270:                track->status = Running;
                    271:        }
                    272:        if (play_event->IsRunning() == false) {
                    273:                // ミキサーイベントが停止してたらイベントを起動する。
                    274:                // 最初の1回は、ブロック境界になるようにする。
1.1.1.2 ! root      275:                play_event->time = BLK_TSEC - (scheduler->GetVirtTime() % BLK_TSEC);
1.1       root      276: 
                    277:                scheduler->StartEvent(play_event);
                    278:                syncer->NotifySoundRunning(true);
                    279:        }
                    280: }
                    281: 
                    282: // 再生を停止する。
                    283: // 各サウンドデバイスが呼ぶ。
                    284: void
                    285: SoundRenderer::StopPlay(SoundTrack *track)
                    286: {
                    287:        if (track->status == Running) {
                    288:                putlog(1, "Draining [%s]", track->GetName().c_str());
                    289:                track->status = Draining;
                    290:        }
                    291: }
                    292: 
                    293: // ブロック先頭の時間に呼ばれて、直前のブロックを処理するコールバック。
                    294: // (VM スレッドで呼ばれる)
                    295: void
                    296: SoundRenderer::Callback(Event *event)
                    297: {
                    298:        // デバイスが全員再生停止していれば
                    299:        // 裏スレッドへの通知も次のイベントループも不要でここで終了。
                    300:        bool playing = false;
                    301:        for (const auto& track : tracks) {
                    302:                if (track.status != Stopped) {
                    303:                        playing = true;
                    304:                        break;
                    305:                }
                    306:        }
                    307:        if (playing == false) {
                    308:                syncer->NotifySoundRunning(false);
                    309:                return;
                    310:        }
                    311: 
                    312:        // ブロック境界を繰り上がったところで呼ばれているので
                    313:        // 処理したいのは一つ前のブロック。
1.1.1.2 ! root      314:        uint64 vseq = (scheduler->GetVirtTime() / BLK_TSEC) - 1;
1.1       root      315: 
                    316:        // 裏(レンダラ)スレッドに通知するだけ。
                    317:        // キューが一杯ならロストするだけ (だけ?)。
                    318:        {
                    319:                std::lock_guard<std::mutex> lock(mtx);
                    320:                request_q.Enqueue(vseq);
                    321:                cv.notify_one();
                    322:        }
                    323: 
                    324:        // 次のイベント。
1.1.1.2 ! root      325:        event->time = BLK_TSEC;
1.1       root      326:        scheduler->StartEvent(event);
                    327: }
                    328: 
                    329: void
                    330: SoundRenderer::Terminate()
                    331: {
                    332:        std::lock_guard<std::mutex> lock(mtx);
                    333:        request |= REQ_TERMINATE;
                    334:        cv.notify_one();
                    335: }
                    336: 
                    337: // サウンドスレッド
                    338: void
                    339: SoundRenderer::ThreadRun()
                    340: {
                    341:        SetThreadAffinityHint(AffinityClass::Heavy);
                    342: 
                    343:        for (;;) {
                    344:                uint32 action = 0;
                    345:                uint64 vseq = -1;
                    346:                {
                    347:                        std::unique_lock<std::mutex> lock(mtx);
                    348:                        cv.wait(lock, [&]() {
                    349:                                // 終了フラグは最優先。
                    350:                                if (__predict_false((request & REQ_TERMINATE))) {
                    351:                                        action = REQ_TERMINATE;
                    352:                                        return true;
                    353:                                }
                    354: 
                    355:                                // ドライバ変更時は出力キューが空である必要がある。
                    356:                                // 入力キューより優先。
                    357:                                if (__predict_false((request & REQ_RECONFIG))) {
                    358:                                        if (hostsnd->IsQEmpty()) {
                    359:                                                request &= ~REQ_RECONFIG;
                    360:                                                action = REQ_RECONFIG;
                    361:                                                return true;
                    362:                                        } else {
                    363:                                                return false;
                    364:                                        }
                    365:                                }
                    366: 
                    367:                                // 入力キューがあれば。
                    368:                                if (request_q.Dequeue(&vseq)) {
                    369:                                        return true;
                    370:                                }
                    371:                                return false;
                    372:                        });
                    373:                }
                    374:                // action がここで処理すべきフラグ。
                    375:                // request のほうは必要なら下げてある。
                    376: 
                    377:                // action == REQ_TERMINATE (終了要求) か
                    378:                // action == REQ_SELECT_DRIVER (ドライバ変更要求) か、
                    379:                // どちらでもなければ vseq >= 0 ならデータあり。
                    380: 
                    381:                if (__predict_false((action & REQ_TERMINATE))) {
                    382:                        break;
                    383:                } else if (__predict_false((action & REQ_RECONFIG))) {
                    384:                        Reconfig(false);
                    385:                } else if ((int64)vseq >= 0) {
                    386:                        Mix(vseq);
                    387:                }
                    388:        }
                    389: }
                    390: 
                    391: // デバイス間合成してホストに出力する。
                    392: // (Sound スレッド)
                    393: void
                    394: SoundRenderer::Mix(uint64 vseq)
                    395: {
                    396:        int16 *dst;
                    397:        uint dstbytes;
                    398: 
                    399:        // 周波数変換はまだないので直接出力バッファに書く。
                    400:        if (1) {
                    401:                dst = outbuf.get() + out_samples_per_blk * outpos;
                    402:                dstbytes = SAMPLES_TO_BYTES(out_samples_per_blk);
                    403:        }
                    404: 
                    405:        // ソースを合成。
                    406:        bool first = true;
                    407:        bool play = false;
                    408:        for (auto& track : tracks) {
                    409:                if (track.status == Stopped) {
                    410:                        continue;
                    411:                }
                    412:                if (track.status == Drained) {
                    413:                        // トラックが前回のブロックで終了したのでレベルメータを下げる。
                    414:                        // 時間軸が無限に流れていれば不要な処理だが、現在の実装だと
                    415:                        // 再生ブロックが終了すると自然とループ自体が止まる仕組みで、
                    416:                        // 再生ループが止まると最後のブロックのピークレベルから
                    417:                        // 書き換わらないことになってしまうので、ここで再生終了後の
                    418:                        // 次の無音ブロックを表現している。
                    419:                        ClearDBFS(track.dbfs);
                    420:                        track.status = Stopped;
                    421:                        continue;
                    422:                }
                    423:                play = true;
                    424: 
                    425:                // このブロック時刻に対応するバッファがあるか。
                    426:                int16 *src = track.GetBlockBySeq(vseq);
                    427:                if (src) {
                    428:                        // デバイスごと生波形についての処理。
                    429: 
                    430:                        // レベルを計算。 
                    431:                        CalcDBFS(track.dbfs, src);
                    432: 
                    433:                        // 合成。
                    434:                        if (first) {
                    435:                                memcpy(dst, src, dstbytes);
                    436:                                first = false;
                    437:                        } else {
                    438:                                AddPCM(dst, src);
                    439:                        }
                    440: 
                    441:                        // デバイスバッファをクリア。
                    442:                        // 排他制御はしていないので一周以上遅れると死ぬ
                    443:                        // (がその状況ではどのみち死ぬのでもう一緒)。
                    444:                        track.ClearSeq(vseq);
                    445:                }
                    446: 
                    447:                // Draining なら最後のブロックを処理したので Drained に移行。
                    448:                // 対応バッファがなくてももう出来ることはないので停止。
                    449:                if (__predict_false(track.status == Draining)) {
                    450:                        // 表示上はここで停止でいいか。
                    451:                        putlog(1, "Stopped [%s]", track.GetName().c_str());
                    452:                        track.status = Drained;
                    453:                }
                    454:        }
                    455: 
                    456:        if (__predict_false(first)) {
                    457:                memset(dst, 0, dstbytes);
                    458:        }
                    459: 
                    460:        // マスターレベルを計算。 
                    461:        // ここまでは Drained のトラックがいても行う。
                    462:        // 再生が終わった次のブロックでメータをゼロにするため。
                    463:        CalcDBFS(master_dbfs, dst);
                    464: 
                    465:        // 1トラック以上が Playing ならホストに出力。
                    466:        if (play) {
                    467:                hostsnd->Play(dst);
                    468:                outpos = (outpos + 1) % NOUTBLKS;
                    469:        }
                    470: }
                    471: 
                    472: // 1ブロックの PCM の合成を行う。dst = dst + src;
                    473: // PCM データはスケールが 1/MAX_SOUND_SOURCES 倍してあるため、
                    474: // 16ビットのまま加算しても溢れない。
                    475: void
                    476: SoundRenderer::AddPCM(int16 *d, const int16 *s)
                    477: {
                    478:        for (uint i = 0; i < mixer_samples_per_blk; i++) {
                    479:                *d = *d + *s;
                    480:                d++;
                    481:                s++;
                    482:        }
                    483: }
                    484: 
                    485: // 出力バッファ 1ブロックのバイト数を返す。
                    486: uint
                    487: SoundRenderer::GetOutBytesPerBlk() const noexcept
                    488: {
                    489:        return SAMPLES_TO_BYTES(out_samples_per_blk);
                    490: }
                    491: 
                    492: // src 1ブロックからチャンネルごとの dbFS を求めて dbfs に書き戻す。
                    493: void
                    494: SoundRenderer::CalcDBFS(std::array<double, NCHAN>& dbfs, const int16 *src)
                    495: {
                    496:        // RMS とピークとあるが、ここではピークを表示する。
                    497: 
                    498:        std::array<int32, NCHAN> ipeak {};
                    499:        for (const int16 *end = src + mixer_samples_per_blk; src < end; ) {
                    500:                for (uint j = 0; j < NCHAN; j++) {
                    501:                        int16 s = *src++;
                    502:                        ipeak[j] = std::max(ipeak[j], (int32)std::abs(s));
                    503:                }
                    504:        }
                    505: 
                    506:        for (uint j = 0; j < NCHAN; j++) {
                    507:                // std::log10(0) の結果は「処理系定義」なので避ける。
                    508:                dbfs[j] = (ipeak[j] == 0)
                    509:                        ? -INFINITY
                    510:                        : 20.0 * std::log10((double)ipeak[j] / 32768);
                    511:        }
                    512: }
                    513: 
                    514: // この dbfs をクリアする。
                    515: /*static*/ void
                    516: SoundRenderer::ClearDBFS(std::array<double, NCHAN>& dbfs)
                    517: {
                    518:        std::fill(dbfs.begin(), dbfs.end(), -INFINITY);
                    519: }
                    520: 
                    521: void
1.1.1.2 ! root      522: SoundRenderer::MonitorScreen(Monitor *, TextScreen& screen)
1.1       root      523: {
                    524:        int y;
                    525: 
                    526:        screen.Clear();
                    527: 
                    528:        // 0123456789012345678901234567890123456789
                    529:        // Master            L:01234567890123456789
                    530:        //                   R:GGGGGGGGGGGGGOOOOORR
                    531:        //
                    532:        // [ADPCM] Running   L:
                    533: 
                    534:        static const char * const statstr[] = {
                    535:                "Stopped",
                    536:                "Drained",
                    537:                "Running",
                    538:                "Draining",
                    539:        };
                    540: 
                    541:        y = 0;
                    542:        screen.Print(0, y, "Master");
1.1.1.2 ! root      543:        MonitorScreenLevelMeter(screen, 18, y, master_dbfs);
1.1       root      544:        y += NCHAN;
                    545: 
                    546:        std::string hline(screen.GetCol(), '-');
                    547:        screen.Puts(0, y++, TA::Disable, hline.c_str());
                    548: 
                    549:        for (const auto& track : tracks) {
                    550:                screen.Print(0, y, "%s", track.GetName().c_str());
                    551:                screen.Print(6, y, "[%s]", statstr[track.status]);
1.1.1.2 ! root      552:                MonitorScreenLevelMeter(screen, 18, y, track.dbfs);
1.1       root      553:                y += (NCHAN + 1);
                    554:        }
                    555: }
                    556: 
                    557: void
1.1.1.2 ! root      558: SoundRenderer::MonitorScreenLevelMeter(TextScreen& screen,
1.1       root      559:        int x, int y, const std::array<double, NCHAN> dbfs)
                    560: {
                    561:        screen.Puts(x, y + 0, "L:");
                    562:        screen.Puts(x, y + 1, "R:");
                    563:        x += 2;
                    564: 
                    565:        for (uint ch = 0; ch < NCHAN; ch++) {
                    566:                // 下地。
                    567:                uint16 bg = ' ' | TA::Off;
                    568:                for (uint i = 0; i < METER_LEN; i++) {
                    569:                        screen.Putc(x + i, y + ch, bg);
                    570:                }
                    571: 
                    572:                // 最小の振幅は -90.3dbFS なので、これ以下は無音。
                    573:                if (dbfs[ch] < -91) {
                    574:                        continue;
                    575:                }
                    576:                // プラス方向に切り上げて丸める。
                    577:                int idbfs = (int)std::ceil(dbfs[ch]);
                    578:                // -60dbFS 付近以下は1レベルにまとめる。
                    579:                int width = METER_LEN + idbfs / METER_DIV;
                    580:                if (width < 1) {
                    581:                        width = 1;
                    582:                }
                    583: 
                    584:                uint16 fg = ' ' | TA::ReverseGreen;
                    585:                for (uint i = 0; i < width; i++) {
                    586:                        // -20dbFS 以上は黄色(オレンジ)、-6dbFS 以上は赤。
                    587:                        if (i == (METER_LEN - (20 / METER_DIV))) {
                    588:                                fg = ' ' | TA::ReverseOrange;
                    589:                        } else if (i == (METER_LEN - (6 / METER_DIV))) {
                    590:                                fg = ' ' | TA::ReverseRed;
                    591:                        }
                    592:                        screen.Putc(x + i, y + ch, fg);
                    593:                }
                    594:        }
                    595: }
1.1.1.2 ! root      596: 
        !           597: void
        !           598: SoundRenderer::MonitorBitmap(Monitor *mon, BitmapRGBX& bitmap)
        !           599: {
        !           600:        // テキストで描いたレベルメータに区切りを入れる。
        !           601: 
        !           602:        uint fw = mon->GetFontWidth();
        !           603:        uint fh = mon->GetFontHeight();
        !           604: 
        !           605:        uint x = 20;
        !           606:        uint yend = (1 + tracks.size()) * 3;
        !           607:        for (uint y = 0; y < yend; y += 3) {
        !           608:                // L,R を区切る横線
        !           609:                uint px = mon->BX(x);
        !           610:                uint py = mon->BY(y + 1);
        !           611:                bitmap.DrawLineH(BGPANEL, px, py, px + fw * METER_LEN);
        !           612: 
        !           613:                // メモリを区切る縦線 (高さ 2CH 分)
        !           614:                for (uint i = 0; i < METER_LEN; i++) {
        !           615:                        px = mon->BX(x + i) + fw - 1;
        !           616:                        py = mon->BY(y);
        !           617:                        bitmap.DrawLineV(BGPANEL, px, py, py + fh * 2);
        !           618:                }
        !           619:        }
        !           620: }

unix.superglobalmegacorp.com

This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.