|
|
nono 1.7.0
//
// nono
// Copyright (C) 2025 nono project
// Licensed under nono-license.txt
//
//
// サウンドレンダラ
//
// vtime / BLK_TSEC をブロック時刻と呼ぶことにする。
//
// 各サウンドトラックは 2 (=NSRCBLKS) ブロック分のバッファを持つ。
// ブロック境界の 000'000_nsec ちょうどにソースデバイスとサウンドレンダラの
// イベントが重なってどちらの順で実行しても構わないようにするため。
//
// サウンドレンダラのソース合成バッファは 1ブロックでいい (NMIXBLKS)。
//
// サウンドレンダラからホストドライバへの出力リングバッファは
// 4 (=NOUTBLKS) ブロック。少なくとも 3 あればいいのできりをよくする。
#include "sound.h"
#include "bitmap.h"
#include "event.h"
#include "hostsound.h"
#include "mainapp.h"
#include "monitor.h"
#include "scheduler.h"
#include "syncer.h"
#include <algorithm>
#include <cmath>
#define SAMPLES_TO_BYTES(samples) ((samples) * sizeof(int16))
#define SAMPLES_TO_FRAMES(samples) ((samples) / NCHAN)
#define FRAMES_TO_SAMPLES(frames) ((frames) * NCHAN)
//
// サウンドトラック
//
// コンストラクタ
SoundTrack::SoundTrack(Device *dev_, size_t mixer_samples_per_blk)
{
dev = dev_;
samples_per_blk = mixer_samples_per_blk;
// 例外はこれをコンストラクトするところで catch してある。
buf.reset(new int16[samples_per_blk * NSRCBLKS]);
std::fill(blkseq.begin(), blkseq.end(), (uint64)-1);
SoundRenderer::ClearDBFS(dbfs);
}
// バッファの idx 番目のブロック先頭を返す。
// 時刻や使用状況によらない。
int16 *
SoundTrack::GetBlockByIdx(uint32 idx)
{
assert(idx < NSRCBLKS);
auto p = buf.get() + idx * samples_per_blk;
return p;
}
// ブロック時刻 vseq のブロック先頭を返す。有効でなければ NULL を返す。
int16 *
SoundTrack::GetBlockBySeq(uint64 vseq)
{
uint32 idx = vseq % NSRCBLKS;
if (blkseq[idx] == vseq) {
return GetBlockByIdx(idx);
}
return NULL;
}
// 時刻 vseq をブロックと紐付けて (すでに紐付いていれば何もしない)、
// このブロックの先頭を返す。
int16 *
SoundTrack::AllocSeq(uint64 vseq)
{
uint32 idx = vseq % NSRCBLKS;
if (__predict_true(blkseq[idx] == (uint64)-1)) {
// バッファは消去済み。
blkseq[idx] = vseq;
} else if (blkseq[idx] != vseq) {
// バッファが未消去なので書きつぶす。(通常起きないはず)
ClearSeq(vseq);
blkseq[idx] = vseq;
}
return GetBlockByIdx(idx);
}
// 時刻 vseq のブロックの紐付けをクリアする。
// (サウンドスレッドから呼ばれるはず)
void
SoundTrack::ClearSeq(uint64 vseq)
{
uint32 idx = vseq % NSRCBLKS;
int16 *blk = GetBlockByIdx(idx);
memset(blk, 0, SAMPLES_TO_BYTES(samples_per_blk));
blkseq[idx] = (int64)-1;
}
//
// サウンドレンダラ
//
// コンストラクタ
SoundRenderer::SoundRenderer()
: inherited(OBJ_SOUND_RENDERER)
{
// ミキサー周波数は機種ごとに違う。
if (gMainApp.IsX68030()) {
// X68030 では 62.5kHz を使う。(adpcm.cpp 参照)
mixerfreq = 62500;
} else {
assert(false);
}
ClearDBFS(master_dbfs);
monitor = gMonitorManager->Regist(ID_MONITOR_SOUND, this);
monitor->SetCallback(&SoundRenderer::MonitorScreen,
&SoundRenderer::MonitorBitmap);
monitor->SetSize(40, 2);
}
// デストラクタ
SoundRenderer::~SoundRenderer()
{
TerminateThread();
}
bool
SoundRenderer::Create()
{
try {
hostsnd.reset(new HostSoundDevice(this));
} catch (...) { }
if ((bool)hostsnd == false) {
warnx("Failed to initialize HostSoundDevice at %s", __method__);
return false;
}
return true;
}
void
SoundRenderer::SetLogLevel(int loglevel_)
{
inherited::SetLogLevel(loglevel_);
if ((bool)hostsnd) {
hostsnd->SetLogLevel(loglevel_);
}
}
// 初期化
bool
SoundRenderer::Init()
{
syncer = GetSyncer();
// トラック合成用バッファ。(1ブロック分)
auto mixer_frames_per_blk = (uint64)mixerfreq * BLK_TSEC / 1_sec;
mixer_samples_per_blk = FRAMES_TO_SAMPLES(mixer_frames_per_blk);
mixbuf.reset(new(std::nothrow) int16[mixer_samples_per_blk]);
if (mixbuf == NULL) {
warnx("Could not allocate %zu bytes at %s",
SAMPLES_TO_BYTES(mixer_samples_per_blk), __method__);
return false;
}
// 出力バッファなど実行時にも変わるものを設定。
request_outfreq = mixerfreq; // XXX 周波数変換はまだ
if (Reconfig(true) == false) {
// エラーメッセージは表示済み。
return false;
}
auto evman = GetEventManager();
play_event = evman->Regist(this,
ToEventCallback(&SoundRenderer::Callback),
"Sound Renderer");
return true;
}
// リセット
void
SoundRenderer::ResetHard(bool poweron)
{
// 各デバイスがリセット時に Stop*() を呼ぶので
// 自動的にこっちも停止するはず。
}
// 出力周波数かホストドライバの変更 (サウンドスレッドで呼ばれる)。
// エラーが起きた場合、
// 起動時なら、warn() 等で表示して false を返す(終了する)。
// 実行中なら、warn() 等で表示してもいいが、必ず None にフォールバックして
// true を返すこと。
bool
SoundRenderer::Reconfig(bool startup)
{
assert(hostsnd->IsQEmpty());
// 出力周波数が変わったら出力リングバッファをリサイズ。
// outbuf は NOUTBLKS ブロック分。
bool need_resize = false;
if (outfreq != request_outfreq) {
outfreq = request_outfreq;
need_resize = true;
}
auto out_frames_per_blk = (uint64)outfreq * BLK_TSEC / 1_sec;
out_samples_per_blk = FRAMES_TO_SAMPLES(out_frames_per_blk);
if (need_resize) {
auto samples = out_samples_per_blk * NOUTBLKS;
outbuf.reset(new(std::nothrow) int16[samples]);
if (outbuf == NULL) {
warnx("Could not allocate %zu bytes at %s",
SAMPLES_TO_BYTES(samples), __method__);
return false;
}
}
// ホストドライバも必ず再切替になる。
if (hostsnd->NotifyReselectDriver() == false) {
warn("%s: NotifyReselectDriver failed?", __method__);
return false;
}
return true;
}
// サウンドトラック(ソースデバイス)を登録する。
// 登録された SoundTrack へのポインタを返す。
// 各ソースデバイスが Init() で呼ぶ。
// Unregist 操作はない。
SoundTrack *
SoundRenderer::RegistTrack(Device *dev)
{
assert(tracks.size() < MAX_SOUND_SOURCES);
try {
// この emplace_back が new SoundTrack(..)。
tracks.emplace_back(dev, mixer_samples_per_blk);
} catch (...) {
warnx("Failed to initialize SoundTrack");
return NULL;
}
monitor->AddHeight(3);
return &tracks.back();
}
// 再生を開始する。
// 各サウンドデバイスが呼ぶ。
void
SoundRenderer::StartPlay(SoundTrack *track)
{
if (track->status != Running) {
putlog(1, "Playing [%s]", track->GetName().c_str());
track->status = Running;
}
if (play_event->IsRunning() == false) {
// ミキサーイベントが停止してたらイベントを起動する。
// 最初の1回は、ブロック境界になるようにする。
play_event->time = BLK_TSEC - (scheduler->GetVirtTime() % BLK_TSEC);
scheduler->StartEvent(play_event);
syncer->NotifySoundRunning(true);
}
}
// 再生を停止する。
// 各サウンドデバイスが呼ぶ。
void
SoundRenderer::StopPlay(SoundTrack *track)
{
if (track->status == Running) {
putlog(1, "Draining [%s]", track->GetName().c_str());
track->status = Draining;
}
}
// ブロック先頭の時間に呼ばれて、直前のブロックを処理するコールバック。
// (VM スレッドで呼ばれる)
void
SoundRenderer::Callback(Event *event)
{
// デバイスが全員再生停止していれば
// 裏スレッドへの通知も次のイベントループも不要でここで終了。
bool playing = false;
for (const auto& track : tracks) {
if (track.status != Stopped) {
playing = true;
break;
}
}
if (playing == false) {
syncer->NotifySoundRunning(false);
return;
}
// ブロック境界を繰り上がったところで呼ばれているので
// 処理したいのは一つ前のブロック。
uint64 vseq = (scheduler->GetVirtTime() / BLK_TSEC) - 1;
// 裏(レンダラ)スレッドに通知するだけ。
// キューが一杯ならロストするだけ (だけ?)。
{
std::lock_guard<std::mutex> lock(mtx);
request_q.Enqueue(vseq);
cv.notify_one();
}
// 次のイベント。
event->time = BLK_TSEC;
scheduler->StartEvent(event);
}
void
SoundRenderer::Terminate()
{
std::lock_guard<std::mutex> lock(mtx);
request |= REQ_TERMINATE;
cv.notify_one();
}
// サウンドスレッド
void
SoundRenderer::ThreadRun()
{
SetThreadAffinityHint(AffinityClass::Heavy);
for (;;) {
uint32 action = 0;
uint64 vseq = -1;
{
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [&]() {
// 終了フラグは最優先。
if (__predict_false((request & REQ_TERMINATE))) {
action = REQ_TERMINATE;
return true;
}
// ドライバ変更時は出力キューが空である必要がある。
// 入力キューより優先。
if (__predict_false((request & REQ_RECONFIG))) {
if (hostsnd->IsQEmpty()) {
request &= ~REQ_RECONFIG;
action = REQ_RECONFIG;
return true;
} else {
return false;
}
}
// 入力キューがあれば。
if (request_q.Dequeue(&vseq)) {
return true;
}
return false;
});
}
// action がここで処理すべきフラグ。
// request のほうは必要なら下げてある。
// action == REQ_TERMINATE (終了要求) か
// action == REQ_SELECT_DRIVER (ドライバ変更要求) か、
// どちらでもなければ vseq >= 0 ならデータあり。
if (__predict_false((action & REQ_TERMINATE))) {
break;
} else if (__predict_false((action & REQ_RECONFIG))) {
Reconfig(false);
} else if ((int64)vseq >= 0) {
Mix(vseq);
}
}
}
// デバイス間合成してホストに出力する。
// (Sound スレッド)
void
SoundRenderer::Mix(uint64 vseq)
{
int16 *dst;
uint dstbytes;
// 周波数変換はまだないので直接出力バッファに書く。
if (1) {
dst = outbuf.get() + out_samples_per_blk * outpos;
dstbytes = SAMPLES_TO_BYTES(out_samples_per_blk);
}
// ソースを合成。
bool first = true;
bool play = false;
for (auto& track : tracks) {
if (track.status == Stopped) {
continue;
}
if (track.status == Drained) {
// トラックが前回のブロックで終了したのでレベルメータを下げる。
// 時間軸が無限に流れていれば不要な処理だが、現在の実装だと
// 再生ブロックが終了すると自然とループ自体が止まる仕組みで、
// 再生ループが止まると最後のブロックのピークレベルから
// 書き換わらないことになってしまうので、ここで再生終了後の
// 次の無音ブロックを表現している。
ClearDBFS(track.dbfs);
track.status = Stopped;
continue;
}
play = true;
// このブロック時刻に対応するバッファがあるか。
int16 *src = track.GetBlockBySeq(vseq);
if (src) {
// デバイスごと生波形についての処理。
// レベルを計算。
CalcDBFS(track.dbfs, src);
// 合成。
if (first) {
memcpy(dst, src, dstbytes);
first = false;
} else {
AddPCM(dst, src);
}
// デバイスバッファをクリア。
// 排他制御はしていないので一周以上遅れると死ぬ
// (がその状況ではどのみち死ぬのでもう一緒)。
track.ClearSeq(vseq);
}
// Draining なら最後のブロックを処理したので Drained に移行。
// 対応バッファがなくてももう出来ることはないので停止。
if (__predict_false(track.status == Draining)) {
// 表示上はここで停止でいいか。
putlog(1, "Stopped [%s]", track.GetName().c_str());
track.status = Drained;
}
}
if (__predict_false(first)) {
memset(dst, 0, dstbytes);
}
// マスターレベルを計算。
// ここまでは Drained のトラックがいても行う。
// 再生が終わった次のブロックでメータをゼロにするため。
CalcDBFS(master_dbfs, dst);
// 1トラック以上が Playing ならホストに出力。
if (play) {
hostsnd->Play(dst);
outpos = (outpos + 1) % NOUTBLKS;
}
}
// 1ブロックの PCM の合成を行う。dst = dst + src;
// PCM データはスケールが 1/MAX_SOUND_SOURCES 倍してあるため、
// 16ビットのまま加算しても溢れない。
void
SoundRenderer::AddPCM(int16 *d, const int16 *s)
{
for (uint i = 0; i < mixer_samples_per_blk; i++) {
*d = *d + *s;
d++;
s++;
}
}
// 出力バッファ 1ブロックのバイト数を返す。
uint
SoundRenderer::GetOutBytesPerBlk() const noexcept
{
return SAMPLES_TO_BYTES(out_samples_per_blk);
}
// src 1ブロックからチャンネルごとの dbFS を求めて dbfs に書き戻す。
void
SoundRenderer::CalcDBFS(std::array<double, NCHAN>& dbfs, const int16 *src)
{
// RMS とピークとあるが、ここではピークを表示する。
std::array<int32, NCHAN> ipeak {};
for (const int16 *end = src + mixer_samples_per_blk; src < end; ) {
for (uint j = 0; j < NCHAN; j++) {
int16 s = *src++;
ipeak[j] = std::max(ipeak[j], (int32)std::abs(s));
}
}
for (uint j = 0; j < NCHAN; j++) {
// std::log10(0) の結果は「処理系定義」なので避ける。
dbfs[j] = (ipeak[j] == 0)
? -INFINITY
: 20.0 * std::log10((double)ipeak[j] / 32768);
}
}
// この dbfs をクリアする。
/*static*/ void
SoundRenderer::ClearDBFS(std::array<double, NCHAN>& dbfs)
{
std::fill(dbfs.begin(), dbfs.end(), -INFINITY);
}
void
SoundRenderer::MonitorScreen(Monitor *, TextScreen& screen)
{
int y;
screen.Clear();
// 0123456789012345678901234567890123456789
// Master L:01234567890123456789
// R:GGGGGGGGGGGGGOOOOORR
//
// [ADPCM] Running L:
static const char * const statstr[] = {
"Stopped",
"Drained",
"Running",
"Draining",
};
y = 0;
screen.Print(0, y, "Master");
MonitorScreenLevelMeter(screen, 18, y, master_dbfs);
y += NCHAN;
std::string hline(screen.GetCol(), '-');
screen.Puts(0, y++, TA::Disable, hline.c_str());
for (const auto& track : tracks) {
screen.Print(0, y, "%s", track.GetName().c_str());
screen.Print(6, y, "[%s]", statstr[track.status]);
MonitorScreenLevelMeter(screen, 18, y, track.dbfs);
y += (NCHAN + 1);
}
}
void
SoundRenderer::MonitorScreenLevelMeter(TextScreen& screen,
int x, int y, const std::array<double, NCHAN> dbfs)
{
screen.Puts(x, y + 0, "L:");
screen.Puts(x, y + 1, "R:");
x += 2;
for (uint ch = 0; ch < NCHAN; ch++) {
// 下地。
uint16 bg = ' ' | TA::Off;
for (uint i = 0; i < METER_LEN; i++) {
screen.Putc(x + i, y + ch, bg);
}
// 最小の振幅は -90.3dbFS なので、これ以下は無音。
if (dbfs[ch] < -91) {
continue;
}
// プラス方向に切り上げて丸める。
int idbfs = (int)std::ceil(dbfs[ch]);
// -60dbFS 付近以下は1レベルにまとめる。
int width = METER_LEN + idbfs / METER_DIV;
if (width < 1) {
width = 1;
}
uint16 fg = ' ' | TA::ReverseGreen;
for (uint i = 0; i < width; i++) {
// -20dbFS 以上は黄色(オレンジ)、-6dbFS 以上は赤。
if (i == (METER_LEN - (20 / METER_DIV))) {
fg = ' ' | TA::ReverseOrange;
} else if (i == (METER_LEN - (6 / METER_DIV))) {
fg = ' ' | TA::ReverseRed;
}
screen.Putc(x + i, y + ch, fg);
}
}
}
void
SoundRenderer::MonitorBitmap(Monitor *mon, BitmapRGBX& bitmap)
{
// テキストで描いたレベルメータに区切りを入れる。
uint fw = mon->GetFontWidth();
uint fh = mon->GetFontHeight();
uint x = 20;
uint yend = (1 + tracks.size()) * 3;
for (uint y = 0; y < yend; y += 3) {
// L,R を区切る横線
uint px = mon->BX(x);
uint py = mon->BY(y + 1);
bitmap.DrawLineH(BGPANEL, px, py, px + fw * METER_LEN);
// メモリを区切る縦線 (高さ 2CH 分)
for (uint i = 0; i < METER_LEN; i++) {
px = mon->BX(x + i) + fw - 1;
py = mon->BY(y);
bitmap.DrawLineV(BGPANEL, px, py, py + fh * 2);
}
}
}
This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.