Wideband autonomous SDR analysis engine forked from sdr-visual-suite
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

431 行
11KB

  1. package recorder
  2. import (
  3. "errors"
  4. "fmt"
  5. "log"
  6. "os"
  7. "path/filepath"
  8. "strings"
  9. "sync"
  10. "time"
  11. "sdr-wideband-suite/internal/demod/gpudemod"
  12. "sdr-wideband-suite/internal/detector"
  13. "sdr-wideband-suite/internal/telemetry"
  14. )
  15. type Policy struct {
  16. Enabled bool `yaml:"enabled" json:"enabled"`
  17. MinSNRDb float64 `yaml:"min_snr_db" json:"min_snr_db"`
  18. MinDuration time.Duration `yaml:"min_duration" json:"min_duration"`
  19. MaxDuration time.Duration `yaml:"max_duration" json:"max_duration"`
  20. PrerollMs int `yaml:"preroll_ms" json:"preroll_ms"`
  21. RecordIQ bool `yaml:"record_iq" json:"record_iq"`
  22. RecordAudio bool `yaml:"record_audio" json:"record_audio"`
  23. AutoDemod bool `yaml:"auto_demod" json:"auto_demod"`
  24. AutoDecode bool `yaml:"auto_decode" json:"auto_decode"`
  25. MaxDiskMB int `yaml:"max_disk_mb" json:"max_disk_mb"`
  26. OutputDir string `yaml:"output_dir" json:"output_dir"`
  27. ClassFilter []string `yaml:"class_filter" json:"class_filter"`
  28. RingSeconds int `yaml:"ring_seconds" json:"ring_seconds"`
  29. // Audio quality (AQ-2, AQ-3, AQ-5)
  30. DeemphasisUs float64 `yaml:"deemphasis_us" json:"deemphasis_us"`
  31. ExtractionTaps int `yaml:"extraction_fir_taps" json:"extraction_fir_taps"`
  32. ExtractionBwMult float64 `yaml:"extraction_bw_mult" json:"extraction_bw_mult"`
  33. DebugLiveAudio bool `yaml:"debug_live_audio" json:"debug_live_audio"`
  34. }
  35. type Manager struct {
  36. mu sync.RWMutex
  37. policy Policy
  38. ring *Ring
  39. sampleRate int
  40. blockSize int
  41. centerHz float64
  42. decodeCommands map[string]string
  43. queue chan detector.Event
  44. gpuDemod *gpudemod.Engine
  45. closed bool
  46. closeOnce sync.Once
  47. workerWG sync.WaitGroup
  48. // Streaming recorder
  49. streamer *Streamer
  50. streamedIDs map[int64]bool // signal IDs that were streamed (skip retroactive recording)
  51. streamedMu sync.Mutex
  52. telemetry *telemetry.Collector
  53. }
  54. func New(sampleRate int, blockSize int, policy Policy, centerHz float64, decodeCommands map[string]string, coll *telemetry.Collector) *Manager {
  55. if policy.OutputDir == "" {
  56. policy.OutputDir = "data/recordings"
  57. }
  58. if policy.RingSeconds <= 0 {
  59. policy.RingSeconds = 8
  60. }
  61. m := &Manager{
  62. policy: policy,
  63. ring: NewRing(sampleRate, blockSize, policy.RingSeconds),
  64. sampleRate: sampleRate,
  65. blockSize: blockSize,
  66. centerHz: centerHz,
  67. decodeCommands: decodeCommands,
  68. queue: make(chan detector.Event, 64),
  69. streamer: newStreamer(policy, centerHz, coll),
  70. streamedIDs: make(map[int64]bool),
  71. telemetry: coll,
  72. }
  73. m.initGPUDemod(sampleRate, blockSize)
  74. m.workerWG.Add(1)
  75. go m.worker()
  76. return m
  77. }
  78. func (m *Manager) Update(sampleRate int, blockSize int, policy Policy, centerHz float64, decodeCommands map[string]string) {
  79. m.mu.Lock()
  80. defer m.mu.Unlock()
  81. prevRingSeconds := m.policy.RingSeconds
  82. m.policy = policy
  83. m.centerHz = centerHz
  84. m.decodeCommands = decodeCommands
  85. // Only reset ring and GPU engine if sample parameters actually changed
  86. needRingReset := m.sampleRate != sampleRate || m.blockSize != blockSize || prevRingSeconds != policy.RingSeconds
  87. m.sampleRate = sampleRate
  88. m.blockSize = blockSize
  89. if needRingReset {
  90. m.initGPUDemodLocked(sampleRate, blockSize)
  91. if m.ring == nil {
  92. m.ring = NewRing(sampleRate, blockSize, policy.RingSeconds)
  93. } else {
  94. m.ring.Reset(sampleRate, blockSize, policy.RingSeconds)
  95. }
  96. } else if m.ring == nil {
  97. m.ring = NewRing(sampleRate, blockSize, policy.RingSeconds)
  98. }
  99. if m.streamer != nil {
  100. m.streamer.updatePolicy(policy, centerHz)
  101. }
  102. if m.telemetry != nil {
  103. m.telemetry.Event("recorder_update", "info", "recorder policy updated", nil, map[string]any{
  104. "sample_rate": sampleRate,
  105. "block_size": blockSize,
  106. "enabled": policy.Enabled,
  107. })
  108. }
  109. }
  110. func (m *Manager) Ingest(t0 time.Time, samples []complex64) {
  111. if m == nil || len(samples) == 0 {
  112. return
  113. }
  114. m.mu.RLock()
  115. ring := m.ring
  116. sampleRate := m.sampleRate
  117. blockSize := m.blockSize
  118. m.mu.RUnlock()
  119. if ring == nil || sampleRate <= 0 {
  120. return
  121. }
  122. chunkSamples := blockSize * 16
  123. if chunkSamples < 65_536 {
  124. chunkSamples = 65_536
  125. }
  126. maxRingSamples := ring.MaxSamples()
  127. if maxRingSamples > 0 && chunkSamples > maxRingSamples {
  128. chunkSamples = maxRingSamples
  129. }
  130. if chunkSamples <= 0 {
  131. chunkSamples = len(samples)
  132. }
  133. for off := 0; off < len(samples); off += chunkSamples {
  134. end := off + chunkSamples
  135. if end > len(samples) {
  136. end = len(samples)
  137. }
  138. chunkStart := t0.Add(time.Duration(float64(off) / float64(sampleRate) * float64(time.Second)))
  139. ring.Push(chunkStart, samples[off:end])
  140. }
  141. if m.telemetry != nil {
  142. m.telemetry.SetGauge("recorder.ring.push_samples", float64(len(samples)), nil)
  143. }
  144. }
  145. func (m *Manager) OnEvents(events []detector.Event) {
  146. if m == nil || len(events) == 0 {
  147. return
  148. }
  149. m.mu.RLock()
  150. enabled := m.policy.Enabled
  151. closed := m.closed
  152. m.mu.RUnlock()
  153. if !enabled || closed {
  154. return
  155. }
  156. for _, ev := range events {
  157. select {
  158. case m.queue <- ev:
  159. default:
  160. // drop if queue full
  161. if m.telemetry != nil {
  162. m.telemetry.IncCounter("recorder.event_queue.drop", 1, nil)
  163. }
  164. }
  165. }
  166. if m.telemetry != nil {
  167. m.telemetry.SetGauge("recorder.event_queue.len", float64(len(m.queue)), nil)
  168. }
  169. }
  170. func (m *Manager) worker() {
  171. defer m.workerWG.Done()
  172. for ev := range m.queue {
  173. _ = m.recordEvent(ev)
  174. }
  175. }
  176. func (m *Manager) initGPUDemod(sampleRate int, blockSize int) {
  177. m.mu.Lock()
  178. defer m.mu.Unlock()
  179. m.initGPUDemodLocked(sampleRate, blockSize)
  180. }
  181. func (m *Manager) gpuEngine() *gpudemod.Engine {
  182. m.mu.RLock()
  183. defer m.mu.RUnlock()
  184. return m.gpuDemod
  185. }
  186. func (m *Manager) initGPUDemodLocked(sampleRate int, blockSize int) {
  187. if m.gpuDemod != nil {
  188. m.gpuDemod.Close()
  189. m.gpuDemod = nil
  190. }
  191. if !gpudemod.Available() {
  192. return
  193. }
  194. eng, err := gpudemod.New(blockSize, sampleRate)
  195. if err != nil {
  196. return
  197. }
  198. m.gpuDemod = eng
  199. }
  200. func (m *Manager) Close() {
  201. if m == nil {
  202. return
  203. }
  204. m.closeOnce.Do(func() {
  205. // Close all active streaming sessions first
  206. if m.streamer != nil {
  207. m.streamer.CloseAll()
  208. }
  209. m.mu.Lock()
  210. m.closed = true
  211. if m.queue != nil {
  212. close(m.queue)
  213. }
  214. gpu := m.gpuDemod
  215. m.gpuDemod = nil
  216. m.mu.Unlock()
  217. m.workerWG.Wait()
  218. if gpu != nil {
  219. gpu.Close()
  220. }
  221. })
  222. }
  223. func (m *Manager) recordEvent(ev detector.Event) error {
  224. // Skip events that were already recorded via streaming
  225. m.streamedMu.Lock()
  226. wasStreamed := m.streamedIDs[ev.ID]
  227. delete(m.streamedIDs, ev.ID) // clean up — event is finished
  228. m.streamedMu.Unlock()
  229. if wasStreamed {
  230. log.Printf("STREAM: skipping retroactive recording for signal %d (already streamed)", ev.ID)
  231. return nil
  232. }
  233. m.mu.RLock()
  234. policy := m.policy
  235. ring := m.ring
  236. sampleRate := m.sampleRate
  237. centerHz := m.centerHz
  238. m.mu.RUnlock()
  239. if !policy.Enabled {
  240. return nil
  241. }
  242. if ev.SNRDb < policy.MinSNRDb {
  243. return nil
  244. }
  245. dur := ev.End.Sub(ev.Start)
  246. if policy.MinDuration > 0 && dur < policy.MinDuration {
  247. return nil
  248. }
  249. if policy.MaxDuration > 0 && dur > policy.MaxDuration {
  250. return nil
  251. }
  252. if len(policy.ClassFilter) > 0 && ev.Class != nil {
  253. match := false
  254. for _, c := range policy.ClassFilter {
  255. if strings.EqualFold(c, string(ev.Class.ModType)) {
  256. match = true
  257. break
  258. }
  259. }
  260. if !match {
  261. return nil
  262. }
  263. }
  264. if !policy.RecordIQ && !policy.RecordAudio {
  265. return nil
  266. }
  267. start := ev.Start.Add(-time.Duration(policy.PrerollMs) * time.Millisecond)
  268. end := ev.End
  269. if start.After(end) {
  270. return errors.New("invalid event window")
  271. }
  272. if ring == nil {
  273. return errors.New("no ring buffer")
  274. }
  275. segment := ring.Slice(start, end)
  276. if len(segment) == 0 {
  277. return errors.New("no iq in ring")
  278. }
  279. dir := filepath.Join(policy.OutputDir, fmt.Sprintf("%s_%0.fHz_evt%d", ev.Start.Format("2006-01-02T15-04-05"), ev.CenterHz, ev.ID))
  280. if err := os.MkdirAll(dir, 0o755); err != nil {
  281. return err
  282. }
  283. files := map[string]any{}
  284. var iqPath string
  285. if policy.RecordIQ {
  286. iqPath = filepath.Join(dir, "signal.cf32")
  287. if err := writeCF32(iqPath, segment); err != nil {
  288. return err
  289. }
  290. files["iq"] = "signal.cf32"
  291. files["iq_format"] = "cf32"
  292. files["iq_sample_rate"] = sampleRate
  293. }
  294. if policy.RecordAudio && policy.AutoDemod && ev.Class != nil {
  295. if err := m.demodAndWrite(dir, ev, segment, files); err != nil {
  296. return err
  297. }
  298. }
  299. if policy.AutoDecode && iqPath != "" && ev.Class != nil {
  300. m.runDecodeIfConfigured(string(ev.Class.ModType), iqPath, sampleRate, files, dir)
  301. }
  302. _ = centerHz
  303. return writeMeta(dir, ev, sampleRate, files)
  304. }
  305. // SliceRecent returns the most recent `seconds` of raw IQ from the ring buffer.
  306. // Returns the IQ samples, sample rate, and center frequency.
  307. func (m *Manager) SliceRecent(seconds float64) ([]complex64, int, float64) {
  308. if m == nil {
  309. return nil, 0, 0
  310. }
  311. m.mu.RLock()
  312. ring := m.ring
  313. sr := m.sampleRate
  314. center := m.centerHz
  315. m.mu.RUnlock()
  316. if ring == nil || sr <= 0 {
  317. return nil, 0, 0
  318. }
  319. end := time.Now()
  320. start := end.Add(-time.Duration(seconds * float64(time.Second)))
  321. iq := ring.Slice(start, end)
  322. return iq, sr, center
  323. }
  324. // FeedSnippets is called once per DSP frame with pre-extracted IQ snippets
  325. // (GPU-accelerated FreqShift+FIR+Decimate). The Streamer handles demod with
  326. // persistent state (overlap-save, stereo decode, de-emphasis) asynchronously.
  327. func (m *Manager) FeedSnippets(items []StreamFeedItem, traceID uint64) {
  328. if m == nil || m.streamer == nil || len(items) == 0 {
  329. return
  330. }
  331. m.mu.RLock()
  332. closed := m.closed
  333. m.mu.RUnlock()
  334. if closed {
  335. return
  336. }
  337. // Mark all signal IDs so recordEvent skips them
  338. m.streamedMu.Lock()
  339. for _, item := range items {
  340. if item.Signal.ID != 0 {
  341. m.streamedIDs[item.Signal.ID] = true
  342. }
  343. }
  344. m.streamedMu.Unlock()
  345. // Convert to internal type
  346. internal := make([]streamFeedItem, len(items))
  347. for i, item := range items {
  348. internal[i] = streamFeedItem{
  349. signal: item.Signal,
  350. snippet: item.Snippet,
  351. snipRate: item.SnipRate,
  352. }
  353. }
  354. m.streamer.FeedSnippets(internal, traceID)
  355. }
  356. // StreamFeedItem is the public type for passing extracted snippets from DSP loop.
  357. type StreamFeedItem struct {
  358. Signal detector.Signal
  359. Snippet []complex64
  360. SnipRate int
  361. }
  362. // Streamer returns the underlying Streamer for live-listen subscriptions.
  363. func (m *Manager) StreamerRef() *Streamer {
  364. if m == nil {
  365. return nil
  366. }
  367. return m.streamer
  368. }
  369. func (m *Manager) ResetStreams() {
  370. if m == nil || m.streamer == nil {
  371. return
  372. }
  373. m.streamer.ResetStreams()
  374. }
  375. func (m *Manager) RuntimeInfoBySignalID() map[int64]RuntimeSignalInfo {
  376. if m == nil || m.streamer == nil {
  377. return nil
  378. }
  379. return m.streamer.RuntimeInfoBySignalID()
  380. }
  381. // ActiveStreams returns info about currently active streaming sessions.
  382. func (m *Manager) ActiveStreams() int {
  383. if m == nil || m.streamer == nil {
  384. return 0
  385. }
  386. return m.streamer.ActiveSessions()
  387. }
  388. // HasListeners returns true if any live-listen subscribers are active or pending.
  389. func (m *Manager) HasListeners() bool {
  390. if m == nil || m.streamer == nil {
  391. return false
  392. }
  393. return m.streamer.HasListeners()
  394. }