nextzlog / todo

ToDo lists for ATS-4, CW4ISR, QxSL, ZyLO.
https://nextzlog.dev
1 stars 0 forks source link

モールス信号の解析で未確定の文字を非表示にする #120

Closed JG1VPP closed 2 years ago

JG1VPP commented 2 years ago

問題意識

モールス信号解読プラグインでは、刻々とデコード結果が変化するが、却ってわかりにくい。

解決方法

無音を検知するまでは、直前の周期で読み取った文字列と一致する部分のみを表示する。

jucky154 commented 2 years ago

以下のような感じでしょうか?

if strings.Contains(直前周期の文字列, 解析で出てきた文字列){
      fmt.Println(直前周期の文字列)
}

if 直前有音 && 今無音{
      fmt.Println(解析で出てきた文字列)
}
直前周期の文字列 = 解析で出てきた文字列

解析が安定しない場合(フェージングや、現状のようにいろんな周波数のピークを取れるようにするため、閾値を0.2にしていることから、他のイシューで挙げた動画のように文字列の前に全く関係ないものがあり、そこら辺は不安定です)には何も表示されずに使えないプラグインになってしまいそうですが大丈夫でしょうか?

JG1VPP commented 2 years ago

閾値Threは周波数の選択で使用されるだけなので、全く関係のない文字が現れることとは関係がないと思います。解析が安定しないというのは、具体的にはどういう状況でしょうか?

jucky154 commented 2 years ago

解析が不安定な件 前回のイシューで建てたように前に音がないとJA1ZLOがOA1ZLOになったりするのはその通りなんですけど、そこのところになにかあると検知しているんだと思うんですが、JA1ZLOの前にRだったりAだったりがついてしまい、ある程度しない(1が解析されるくらいまで)その文字は一定にならないという感じです(ノイズなんで)

以下の動画の二局目のJR8の前に何かついていたりします。(JA1ZLOでもタイミングによってはつきます)

https://user-images.githubusercontent.com/58735989/197808509-93b7fa0e-93c7-42aa-b58f-234f5a267a74.mp4

そもそも未確定文字の非表示について 現状のCWプラグインでは7MHzでは対応できないくらい遅いというのがZLOの部員の意見でした

さすがに7Mとかこの間だと厳しいかも、特にパイル時とか次の呼びかけが始まりそう

それを踏まえると確定するまで非表示は表示が遅れてしまうので、あまりよろしくないのかなと思いました (例えば二文字一気に解析された場合は二文字の表示が遅れることになるので致命的)

もちろん

super checkみたいに補助として使うならいい感じ 1局消化したあとQRZ?の代わりに裏の局を呼び出せるのでQSOs/hr上がりそうだね

という評価はもらいましたし、21MHzとかならいけそうという話ではありました。

コード VPPさんが想定されているのは上の方法ということでしょうか?

JG1VPP commented 2 years ago

今、問題にしているのは、0.5秒周期で解析している都合により、特に最後に読み取った文字は信頼できないので、解析結果が固まるまでは非表示にすべきだということです。

var prev_text string
var latest_text string
var shown_text string

for i := len(prev_text); i >= 1; i-- {
    if strings.HasPrefix(latest_text, prev_text[:i]) {
        shown_text = prev_text[:i]
        break
    }
}

JA1ZLOの前にRだったりAだったりがついてしまい、ある程度しない(1が解析されるくらいまで)その文字は一定にならないという感じです(ノイズなんで)

仮に何かノイズを拾っているとして、そのノイズの正体を明らかにして、対策を講じるべきです。

現状のCWプラグインでは7MHzでは対応できないくらい遅いというのがZLOの部員の意見でした

遅い、というのと、安定しない、というのは、別の現象を指していて、その対策は矛盾しないように思います。まずは原因を考察しましょう。対応できないくらい遅いとか言われたら、自分なら悔しくて徹底的に対策しますが。

現役部員には、自由にIssueを立ててもらってください。OSSなのにクローズドな開発をするのはもったいないです。

jucky154 commented 2 years ago

解析が固まるまで表示しない そのように書いています 上のコードだと、最後の確定のタイミング(有音から無音に切り替わるタイミング)では最後の文字がチェックされないため、最後の文字が表示されないという問題があるので、最後の確定のタイミング(有音から無音に切り替わるタイミング)では何もせずに返すようにしないといけないですね

ノイズ的なもの 調査します

遅いという件 考えます… CWスキーマがどういう感じなのかが気になりますね(偉大なる巨人の肩に乗りたい) issueを立てましょうと推奨します

jucky154 commented 2 years ago

それなりに確度が高い文字のみを表示する コードです https://github.com/jucky154/cwListener/blob/main/cwListener.go

動画のようになりました

https://user-images.githubusercontent.com/58735989/197825440-0a7ac13c-71cb-4e0b-9681-c9f5fb226627.mp4

JG1VPP commented 2 years ago

動画を見ると、0.5秒では説明が付かない遅延がありますが、実際その通りですか?

jucky154 commented 2 years ago

実際その通りですか?

どういう意味でしょうか? 音はpowerpointもプラグインも同じラインで入れていて、かつ、動画は実時間でやっていますので、この時間の感覚です。

https://github.com/nextzlog/todo/issues/120#issuecomment-1290683762 リンク先の動画が確定した文字列とか考えずに表示する場合です。時間の感覚としてどうなんでしょうか?

jucky154 commented 2 years ago

recordtime = 0.6 //リングバッファの時間[s] リングバッファの時間(全くもってリングバッファは使っていないがコメントアウトがその名残に)が0.6秒になっていますね… それはあるかもしれないです

JG1VPP commented 2 years ago

つまり、Read関数自体の遅延は無視できるという理解で合っていますか?

jucky154 commented 2 years ago

一応は、0.5秒でコンパイルし直したのが以下です 若干早くなりましたね Read関数自体よりも待機時間の方が律速な感じですね(雰囲気は)

https://user-images.githubusercontent.com/58735989/197831729-f1f156d4-2b7f-4927-b171-1940551e8c7d.mp4

jucky154 commented 2 years ago

例えば、現状でも半分の0.25秒くらいまで音が入っていて有音とされた場合は、次の0.5秒まで合わせて0.75秒(0.6秒でやっていた時は0.9秒)間は確定されないので、その辺がワンテンポ遅れるとかボトルネック的なものになっていてもおかしくはないと思います。

JG1VPP commented 2 years ago

そもそも、所詮1次元のクラスタリングに50回もイテレーションを回すのはやり過ぎで、10回でも多すぎる気がします。また、Read関数の遅延時間を測定してみて、どの程度まで周期を短くできるか検討しましょう。

例えば、0.1秒周期でも問題ないのであれば、0.1秒周期にしましょう。それで、UXが改善する筈です。

無音判定は、例えば、12WPMの場合、短点1個の時間は0.1秒で、文字の区切りは0.3秒なので、閾値にも依りますが、最長でも0.6秒待てば文章が終了したかがわかります。つまり、無音が6回続けば待機状態に戻れる訳ですよね。

jucky154 commented 2 years ago

現状は以下の通りなので

    if peak <= mean*m.Squelch {
        m.storage = signal
        m.counter = 0
        return text, true
    } else {
        return text, false
    }

をいい感じに書き換える必要がありますね

あと周期を短くすれば、m.storage = signalで入れる量が短くなり余計なものを入れなくなるので、誤解析も防げそうな感じがしますね

jucky154 commented 2 years ago

こんな感じですかね(zylo/morse側ですけど) 無音であっても一定回数ないであればfalseを返してほしいです(実際に確定していないという意味で)

m.cnt_silent (無音判定が有音から何回繰り返されたか)
m.cnt_wait (待機状態に戻るための無音回数)

switch {
case peak <= mean*m.Squelch  && m.cnt_silent => m.cnt_wait :
     m.storage = signal
     m.counter = 0
     return text, true
case peak <= mean*m.Squelch  && m.cnt_silent < m.cnt_wait :
     m.cnt_silent += 1
     return text, false
default :
     m.cnt_silent = 0
     return text, false
}
jucky154 commented 2 years ago

時間計測について

    now := time.Now()
    decode_result, mute_bool := monitor.Read(signal)
    duration := time.Since(now).Milliseconds()

以上のようなコードを入れて、その結果を3列目に表示するようにしました

実験環境など ・intel core5 第5世代(zLog利用者に合わせるため) ・周期は0.5秒です ・イタレーションは10回です

結果 最初は100ms弱で最後の方は500msに近くになってくるので、周期が0.5秒というのは案外、限界だったりしそうですね… イタレーションは50でも10でも時間はあんまり変わらなかったです…

https://user-images.githubusercontent.com/58735989/197940032-695f9e05-6531-402d-9356-69bda389bdaa.mp4

jucky154 commented 2 years ago
    decoder = morse.Decoder{
        Thre: 0.2,
        Bias: 20,
        Iter: 50,
        STFT: &stft.STFT{
            FrameShift: int(float64(rate_sound) / float64(100)),
            FrameLen:   4096,
            Window:     window.CreateHanning(4096),
        },
    }

を人間が打てないCWを解析できる能力を失いますが、以下のようにshiftを下げ、バッファーを0.3秒にしました

    decoder = morse.Decoder{
        Thre: 0.2,
        Bias: 20,
        Iter: 20,
        STFT: &stft.STFT{
            FrameShift: int(float64(rate_sound) / float64(50)),
            FrameLen:   4096,
            Window:     window.CreateHanning(4096),
        },
    }

ちょっと早くなりましたかね?  無音のところの処理を書いていないので、たまによくないタイミングで確定されます

https://user-images.githubusercontent.com/58735989/197943869-f3e7e0fc-bcc1-45fc-bb38-6b3bb9badc98.mp4

jucky154 commented 2 years ago

7MHzでも使えるかという質問をZLO部員(正確には私と同期で老害なので、表立ってZLOの場で活動したくないため、このissueには遠くから回答したいとのこと)したところ

だいぶ早くなったすね これなら問題なさそう

という回答を得ました もうCWerがいらない可能性が見えてきたかもしれません

JG1VPP commented 2 years ago

無音の判定アルゴリズムを以下のように修正しました:

  1. 全周波数のパワーのうちThre以上を占める周波数を選ぶ
  2. そのような周波数が存在しない場合は無音
  3. 解析を行う
  4. 解析結果が全て空白文字で終了する場合は確定

使用例です:

package main

import (
    "fmt"
    "github.com/faiface/beep/speaker"
    "github.com/mjibson/go-dsp/wav"
    "github.com/r9y9/gossp/stft"
    "github.com/r9y9/gossp/window"
    "os"
    "strings"
    "zylo/morse"
)

const rate = 8000

func main() {
    file, _ := os.Open("pileup001.wav")
    wave, _ := wav.New(file)
    data, _ := wave.ReadFloats(wave.Samples)
    signal := make([]float64, wave.Samples)
    for i, val := range data {
        signal[i] = float64(val)
    }

    decoder := morse.Decoder{
        Thre: 0.03,
        Iter: 10,
        Bias: 2,
        STFT: &stft.STFT{
            FrameShift: rate / 100,
            FrameLen:   4096,
            Window:     window.CreateHanning(4096),
        },
    }

    speaker.Init(rate, 256)

    monitor := morse.Monitor{
        MaxHold: rate * 60,
        Decoder: decoder,
    }

    chunk := len(signal) / 5

    for n := 0; n < 5; n++ {
        finish := true
        sig := signal[n*chunk : (n+1)*chunk]
        result := monitor.Read(sig)
        for _, code := range result {
            if !strings.HasSuffix(CodeToText(code), " ") {
                finish = false
            }
        }
        if finish {
            for _, code := range result {
                fmt.Println(code)
                fmt.Println(CodeToText(code))
            }
        }
    }
}
JG1VPP commented 2 years ago

計算時間のボトルネックはFFTだと思います。FFTの計算量はnlognで、周波数解像度を妥協してウィンドウ幅を小さくすれば、処理時間が改善するのではないでしょうか?

jucky154 commented 2 years ago

https://gist.github.com/jucky154/53f1906d2959d5ff0cea541fd3db28e0 と書き換えて、

    decoder := morse.Decoder{
        Thre: 0.03,
        Iter: 10,
        Bias: 2,
        STFT: &stft.STFT{
            FrameShift: rate / 50,
            FrameLen:   2048,
            Window:     window.CreateHanning(2048),
        },
    }

で実行したところJA1ZLO程度の短さなら80ms程度で終わっているようですね。(もっと長いR UR 599 100110 H BKとか来た場合にどうなるかを検討する必要はありそうです) そのため、0.2秒周期くらいにしても良さそうな気がしますね

→そのようにする場合は内部の無音判定は何回か無音が来たら無音とするみたいな感じへの修正をお願いします。

その他の問題点としては、動画のようにfinishがうまくいっていません

    finish := true
    morse_texts := make([]string, 0)
    for i, code := range decode_result {
        morse_texts = append(morse_texts, morse.CodeToText(code)) 
        if !strings.HasSuffix(morse_texts[i], " ") {
            finish = false
        }
    }

と書いて、上とほぼ同じように対応していると思うのですが、おそらくdecoder内部では解析中(無音と判断しているのであればJA1ZLOと続けて書かれない)と判断しているのにもかかわらず、finishがtrueで帰ってきてしまい、動画のように無意味な改行や、finish時には点検なしで表示するところ関係で不安定な文字列を表示してしまっています。

https://user-images.githubusercontent.com/58735989/198077179-7084a5b4-d519-4d5a-8b90-a4c32af75860.mp4

JG1VPP commented 2 years ago

finishの件はさておき、想定としては、閾値の調整により、無音判定が正確になる筈です。0.03よりも大きな値にすると余計な文字は消えますか?

JG1VPP commented 2 years ago

finishの判定は、上にあるように、末尾に空白文字が付与されるかで決まるので、デバッグ方法としては、空白文字を可視化するか、CodeToText関数を使わずに、末尾に;が挿入されているかを確認しましょう。

JG1VPP commented 2 years ago

恐らく、読み取れずにCodeToText関数でモールス信号の符号語をそのまま出力している場合に、末尾に空白文字が追加されてしまうために、finishtrueになっているのでしょう。対策としては、CodeToText関数を使わずに判定するか、CodeToText関数を修正するかですが、もともとCodeToText関数の想定用途ではないですし、と言って、;で終わる文字列なら読み取り完了、とするのも不親切ですよね。

jucky154 commented 2 years ago

codetotextなし 下の三つは別の音のせいでこうなってしまったんですが、それ以外を見ると、普通に;はないですね…

JG1VPP commented 2 years ago

それはcorrect_string関数を通した値を表示しているからではないですか?

jucky154 commented 2 years ago

通さないように書き換えた結果が上です。 (本当に表示部は何もしていないcodeが表示されています)

JG1VPP commented 2 years ago

Monitor.Read関数の内部では、末尾に;の3文字がある場合に送信終了というように判定しているので、finishの挙動と一致しなければおかしいです。

jucky154 commented 2 years ago

https://gist.github.com/jucky154/29e03126a97feebef27dde4a07f84b58 がコードで、変更した部分はここですね…

for i := 0; i < int(math.Min(float64(len(decode_result)), float64(2))); i++ {
        latest_text := decode_result[i]
        switch i + 1 {
        case 1:
            cwitems.morseresult1 = latest_text
        case 2:
            cwitems.morseresult2 = latest_text
        case 3:
            cwitems.morseresult3 = latest_text
        }
    }

/*
    for i := 0; i < int(math.Min(float64(len(decode_result)), float64(2))); i++ {
        latest_text := morse_texts[i]
        switch i + 1 {
        case 1:
            cwitems.morseresult1 = correct_string(prev_texts[i], latest_text, finish)
        case 2:
            cwitems.morseresult2 = correct_string(prev_texts[i], latest_text, finish)
        case 3:
            cwitems.morseresult3 = correct_string(prev_texts[i], latest_text, finish)
        }
        prev_texts[i] = latest_text
    }
*/
jucky154 commented 2 years ago
    finish := true
    morse_texts := make([]string, 0)
    for i, code := range decode_result {
        morse_texts = append(morse_texts, morse.CodeToText(code)) 
        if !strings.HasSuffix(morse_texts[i], " ") {
            finish = false
        }
    }

この書き方が良くないとかですかね? appendすると最後の無意味な空白文字が消えるとか、morse_texts[i]ではなくmorse_texts[i+1]呼ぶべき?

JG1VPP commented 2 years ago

codeの末尾に空白文字がないですか?それをstrings.Split(code, " ")した際に、最後が空文字列となり、それに対応してCodeToText関数が空白文字を挿入した可能性があります。

jucky154 commented 2 years ago

調査しました テキトーに文字列の最後に/を入れてみました 一番右の列に比較用に_/という空白のない場合を作ってみました すると、スペースがあるぽいので、VPPさんの想定通りと思われます strings.TrimSpaceをすればいいということでしょうか?

調査

JG1VPP commented 2 years ago

当面は;の3文字がsuffixになっているかで判定してください。

JG1VPP commented 2 years ago

流石に酷い仕様なので、Message.Finish()という関数を用意しました。というか、Monitor.Read関数からstringではなくMessageを返すように変更しました。ここには、周波数の情報も含まれていますが、正確にはFFTのインデックスなので、サンプリング周波数を窓長で割った係数を掛けるとHz単位になります。

jucky154 commented 2 years ago

それに合わせてプラグインも書き換えました https://gist.github.com/jucky154/8c24cd3e843a8382dddb8efe889c090f

無音なのに停止しなくなりました わざと閾値を0.5に設定して、実行したのが以下の動画なんですけど、無音なのに正しく無音判定がなされず、実行時間がただひたすらに伸びていることがわかります(一番右の列はread関数の実行時間)つまり、適切に無音判定が行われず、音ありと判断され、ひたすらにsignalが連結されて行っています

https://user-images.githubusercontent.com/58735989/198282411-4690eeab-9e5f-4068-9856-2dab908ea800.mp4

jucky154 commented 2 years ago

「ノイズのみ」ということはFinishが一つ前:trueで現在 : trueということなんですけど…

JG1VPP commented 2 years ago

無音判定、というより文の終わりの判定ですが、誤って真偽が逆転していたので、修正しました

jucky154 commented 2 years ago

適切に動きました 動画のようになりました 無音判定が早い都合でコールサインが分かれてしまいましたが、結構よさげですね

https://user-images.githubusercontent.com/58735989/198298449-d712bb33-5ab2-4c67-b192-ece6e5704d0c.mp4

JG1VPP commented 2 years ago

対策として、文の区切りの空白の判定基準を引き上げました

適切に動きました 動画のようになりました 無音判定が早い都合でコールサインが分かれてしまいましたが、結構よさげですね