Wideband autonomous SDR analysis engine forked from sdr-visual-suite
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

751 строка
19KB

  1. package recorder
  2. import (
  3. "bufio"
  4. "encoding/binary"
  5. "encoding/json"
  6. "fmt"
  7. "log"
  8. "math"
  9. "os"
  10. "path/filepath"
  11. "strings"
  12. "sync"
  13. "time"
  14. "sdr-visual-suite/internal/classifier"
  15. "sdr-visual-suite/internal/demod"
  16. "sdr-visual-suite/internal/detector"
  17. "sdr-visual-suite/internal/dsp"
  18. )
  19. // ---------------------------------------------------------------------------
  20. // streamSession — one open recording for one signal
  21. // ---------------------------------------------------------------------------
  22. type streamSession struct {
  23. signalID int64
  24. centerHz float64
  25. bwHz float64
  26. snrDb float64
  27. peakDb float64
  28. class *classifier.Classification
  29. startTime time.Time
  30. lastFeed time.Time
  31. dir string
  32. wavFile *os.File
  33. wavBuf *bufio.Writer
  34. wavSamples int64
  35. sampleRate int // actual output audio sample rate
  36. channels int
  37. demodName string
  38. segmentIdx int
  39. // --- Persistent DSP state for click-free streaming ---
  40. // Overlap-save: tail of previous extracted IQ snippet.
  41. // Prepended to the next snippet so FIR filters and FM discriminator
  42. // have history — eliminates transient clicks at frame boundaries.
  43. overlapIQ []complex64
  44. // De-emphasis IIR state (persists across frames)
  45. deemphL float64
  46. deemphR float64
  47. // Stereo decode: phase-continuous 38kHz oscillator
  48. stereoPhase float64
  49. // live-listen subscribers
  50. audioSubs []audioSub
  51. }
  52. type audioSub struct {
  53. id int64
  54. ch chan []byte
  55. }
  56. const (
  57. streamAudioRate = 48000
  58. )
  59. // ---------------------------------------------------------------------------
  60. // Streamer — manages all active streaming sessions
  61. // ---------------------------------------------------------------------------
  62. type streamFeedItem struct {
  63. signal detector.Signal
  64. snippet []complex64
  65. snipRate int
  66. }
  67. type streamFeedMsg struct {
  68. items []streamFeedItem
  69. }
  70. type Streamer struct {
  71. mu sync.Mutex
  72. sessions map[int64]*streamSession
  73. policy Policy
  74. centerHz float64
  75. nextSub int64
  76. feedCh chan streamFeedMsg
  77. done chan struct{}
  78. }
  79. func newStreamer(policy Policy, centerHz float64) *Streamer {
  80. st := &Streamer{
  81. sessions: make(map[int64]*streamSession),
  82. policy: policy,
  83. centerHz: centerHz,
  84. feedCh: make(chan streamFeedMsg, 2),
  85. done: make(chan struct{}),
  86. }
  87. go st.worker()
  88. return st
  89. }
  90. func (st *Streamer) worker() {
  91. for msg := range st.feedCh {
  92. st.processFeed(msg)
  93. }
  94. close(st.done)
  95. }
  96. func (st *Streamer) updatePolicy(policy Policy, centerHz float64) {
  97. st.mu.Lock()
  98. defer st.mu.Unlock()
  99. wasEnabled := st.policy.Enabled
  100. st.policy = policy
  101. st.centerHz = centerHz
  102. // If recording was just disabled, close all active sessions
  103. // so WAV headers get fixed and meta.json gets written.
  104. if wasEnabled && !policy.Enabled {
  105. for id, sess := range st.sessions {
  106. for _, sub := range sess.audioSubs {
  107. close(sub.ch)
  108. }
  109. sess.audioSubs = nil
  110. closeSession(sess, &st.policy)
  111. delete(st.sessions, id)
  112. }
  113. log.Printf("STREAM: recording disabled — closed %d sessions", len(st.sessions))
  114. }
  115. }
  116. // FeedSnippets is called from the DSP loop with pre-extracted IQ snippets
  117. // (GPU-accelerated FreqShift+FIR+Decimate already done). It copies the snippets
  118. // and enqueues them for async demod in the worker goroutine.
  119. func (st *Streamer) FeedSnippets(items []streamFeedItem) {
  120. st.mu.Lock()
  121. enabled := st.policy.Enabled && (st.policy.RecordAudio || st.policy.RecordIQ)
  122. st.mu.Unlock()
  123. if !enabled || len(items) == 0 {
  124. return
  125. }
  126. // Copy snippets (GPU buffers may be reused)
  127. copied := make([]streamFeedItem, len(items))
  128. for i, item := range items {
  129. snipCopy := make([]complex64, len(item.snippet))
  130. copy(snipCopy, item.snippet)
  131. copied[i] = streamFeedItem{
  132. signal: item.signal,
  133. snippet: snipCopy,
  134. snipRate: item.snipRate,
  135. }
  136. }
  137. select {
  138. case st.feedCh <- streamFeedMsg{items: copied}:
  139. default:
  140. // Worker busy — drop frame rather than blocking DSP loop
  141. }
  142. }
  143. // processFeed runs in the worker goroutine. Receives pre-extracted snippets
  144. // and does the lightweight demod + stereo + de-emphasis with persistent state.
  145. func (st *Streamer) processFeed(msg streamFeedMsg) {
  146. st.mu.Lock()
  147. defer st.mu.Unlock()
  148. if !st.policy.Enabled || (!st.policy.RecordAudio && !st.policy.RecordIQ) {
  149. return
  150. }
  151. now := time.Now()
  152. seen := make(map[int64]bool, len(msg.items))
  153. for i := range msg.items {
  154. item := &msg.items[i]
  155. sig := &item.signal
  156. seen[sig.ID] = true
  157. if sig.ID == 0 || sig.Class == nil {
  158. continue
  159. }
  160. if sig.SNRDb < st.policy.MinSNRDb {
  161. continue
  162. }
  163. if !st.classAllowed(sig.Class) {
  164. continue
  165. }
  166. if len(item.snippet) == 0 || item.snipRate <= 0 {
  167. continue
  168. }
  169. sess, exists := st.sessions[sig.ID]
  170. if !exists {
  171. s, err := st.openSession(sig, now)
  172. if err != nil {
  173. log.Printf("STREAM: open failed signal=%d %.1fMHz: %v",
  174. sig.ID, sig.CenterHz/1e6, err)
  175. continue
  176. }
  177. st.sessions[sig.ID] = s
  178. sess = s
  179. }
  180. // Update metadata
  181. sess.lastFeed = now
  182. sess.centerHz = sig.CenterHz
  183. sess.bwHz = sig.BWHz
  184. if sig.SNRDb > sess.snrDb {
  185. sess.snrDb = sig.SNRDb
  186. }
  187. if sig.PeakDb > sess.peakDb {
  188. sess.peakDb = sig.PeakDb
  189. }
  190. if sig.Class != nil {
  191. sess.class = sig.Class
  192. }
  193. // Demod with persistent state (overlap-save, stereo, de-emphasis)
  194. audio, audioRate := sess.processSnippet(item.snippet, item.snipRate)
  195. if len(audio) > 0 {
  196. if sess.wavSamples == 0 && audioRate > 0 {
  197. sess.sampleRate = audioRate
  198. }
  199. appendAudio(sess, audio)
  200. st.fanoutAudio(sess, audio)
  201. }
  202. // Segment split
  203. if st.policy.MaxDuration > 0 && now.Sub(sess.startTime) >= st.policy.MaxDuration {
  204. segIdx := sess.segmentIdx + 1
  205. oldSubs := sess.audioSubs
  206. oldOverlap := sess.overlapIQ
  207. oldDeemphL := sess.deemphL
  208. oldDeemphR := sess.deemphR
  209. oldStereo := sess.stereoPhase
  210. sess.audioSubs = nil
  211. closeSession(sess, &st.policy)
  212. s, err := st.openSession(sig, now)
  213. if err != nil {
  214. delete(st.sessions, sig.ID)
  215. continue
  216. }
  217. s.segmentIdx = segIdx
  218. s.audioSubs = oldSubs
  219. s.overlapIQ = oldOverlap
  220. s.deemphL = oldDeemphL
  221. s.deemphR = oldDeemphR
  222. s.stereoPhase = oldStereo
  223. st.sessions[sig.ID] = s
  224. }
  225. }
  226. // Close sessions for disappeared signals (with grace period)
  227. for id, sess := range st.sessions {
  228. if seen[id] {
  229. continue
  230. }
  231. if now.Sub(sess.lastFeed) > 3*time.Second {
  232. closeSession(sess, &st.policy)
  233. delete(st.sessions, id)
  234. }
  235. }
  236. }
  237. // CloseAll finalises all sessions and stops the worker goroutine.
  238. func (st *Streamer) CloseAll() {
  239. // Stop accepting new feeds and wait for worker to finish
  240. close(st.feedCh)
  241. <-st.done
  242. st.mu.Lock()
  243. defer st.mu.Unlock()
  244. for id, sess := range st.sessions {
  245. for _, sub := range sess.audioSubs {
  246. close(sub.ch)
  247. }
  248. sess.audioSubs = nil
  249. closeSession(sess, &st.policy)
  250. delete(st.sessions, id)
  251. }
  252. }
  253. // ActiveSessions returns the number of open streaming sessions.
  254. func (st *Streamer) ActiveSessions() int {
  255. st.mu.Lock()
  256. defer st.mu.Unlock()
  257. return len(st.sessions)
  258. }
  259. // SubscribeAudio registers a live-listen subscriber for a given frequency.
  260. func (st *Streamer) SubscribeAudio(freq float64, bw float64, mode string) (int64, <-chan []byte) {
  261. ch := make(chan []byte, 64)
  262. st.mu.Lock()
  263. defer st.mu.Unlock()
  264. st.nextSub++
  265. subID := st.nextSub
  266. var bestSess *streamSession
  267. bestDist := math.MaxFloat64
  268. for _, sess := range st.sessions {
  269. d := math.Abs(sess.centerHz - freq)
  270. if d < bestDist {
  271. bestDist = d
  272. bestSess = sess
  273. }
  274. }
  275. if bestSess != nil && bestDist < 200000 {
  276. bestSess.audioSubs = append(bestSess.audioSubs, audioSub{id: subID, ch: ch})
  277. } else {
  278. log.Printf("STREAM: audio subscriber %d has no matching session (freq=%.1fMHz)", subID, freq/1e6)
  279. close(ch)
  280. }
  281. return subID, ch
  282. }
  283. // UnsubscribeAudio removes a live-listen subscriber.
  284. func (st *Streamer) UnsubscribeAudio(subID int64) {
  285. st.mu.Lock()
  286. defer st.mu.Unlock()
  287. for _, sess := range st.sessions {
  288. for i, sub := range sess.audioSubs {
  289. if sub.id == subID {
  290. close(sub.ch)
  291. sess.audioSubs = append(sess.audioSubs[:i], sess.audioSubs[i+1:]...)
  292. return
  293. }
  294. }
  295. }
  296. }
  297. // ---------------------------------------------------------------------------
  298. // Session: stateful extraction + demod
  299. // ---------------------------------------------------------------------------
  300. // processSnippet takes a pre-extracted IQ snippet (from GPU or CPU
  301. // extractSignalIQBatch) and demodulates it with persistent state.
  302. //
  303. // The overlap-save operates on the EXTRACTED snippet level: we prepend
  304. // the tail of the previous snippet so that:
  305. // - FM discriminator has iq[i-1] for the first sample
  306. // - The ~50-sample transient from FreqShift phase reset and FIR startup
  307. // falls into the overlap region and gets trimmed from the output
  308. //
  309. // Stateful components (across frames):
  310. // - overlapIQ: tail of previous extracted snippet
  311. // - stereoPhase: 38kHz oscillator for L-R decode
  312. // - deemphL/R: de-emphasis IIR accumulators
  313. func (sess *streamSession) processSnippet(snippet []complex64, snipRate int) ([]float32, int) {
  314. if len(snippet) == 0 || snipRate <= 0 {
  315. return nil, 0
  316. }
  317. isWFMStereo := sess.demodName == "WFM_STEREO"
  318. isWFM := sess.demodName == "WFM" || isWFMStereo
  319. demodName := sess.demodName
  320. if isWFMStereo {
  321. demodName = "WFM" // mono FM demod, then stateful stereo post-process
  322. }
  323. d := demod.Get(demodName)
  324. if d == nil {
  325. d = demod.Get("NFM")
  326. }
  327. if d == nil {
  328. return nil, 0
  329. }
  330. // --- Minimal overlap: prepend last sample from previous snippet ---
  331. // The FM discriminator computes atan2(iq[i] * conj(iq[i-1])), so the
  332. // first output sample needs iq[-1] from the previous frame.
  333. // FIR halo is already handled by extractForStreaming's IQ-level overlap,
  334. // so we only need 1 sample here.
  335. var fullSnip []complex64
  336. trimSamples := 0
  337. if len(sess.overlapIQ) > 0 {
  338. fullSnip = make([]complex64, len(sess.overlapIQ)+len(snippet))
  339. copy(fullSnip, sess.overlapIQ)
  340. copy(fullSnip[len(sess.overlapIQ):], snippet)
  341. trimSamples = len(sess.overlapIQ)
  342. } else {
  343. fullSnip = snippet
  344. }
  345. // Save last sample for next frame's FM discriminator
  346. if len(snippet) > 0 {
  347. sess.overlapIQ = []complex64{snippet[len(snippet)-1]}
  348. }
  349. // --- Decimate to demod-preferred rate with anti-alias ---
  350. demodRate := d.OutputSampleRate()
  351. decim1 := int(math.Round(float64(snipRate) / float64(demodRate)))
  352. if decim1 < 1 {
  353. decim1 = 1
  354. }
  355. actualDemodRate := snipRate / decim1
  356. var dec []complex64
  357. if decim1 > 1 {
  358. cutoff := float64(actualDemodRate) / 2.0 * 0.8
  359. aaTaps := dsp.LowpassFIR(cutoff, snipRate, 101)
  360. filtered := dsp.ApplyFIR(fullSnip, aaTaps)
  361. dec = dsp.Decimate(filtered, decim1)
  362. } else {
  363. dec = fullSnip
  364. }
  365. // --- FM Demod ---
  366. audio := d.Demod(dec, actualDemodRate)
  367. if len(audio) == 0 {
  368. return nil, 0
  369. }
  370. // --- Trim the overlap sample(s) from audio ---
  371. audioTrim := trimSamples / decim1
  372. if decim1 <= 1 {
  373. audioTrim = trimSamples
  374. }
  375. if audioTrim > 0 && audioTrim < len(audio) {
  376. audio = audio[audioTrim:]
  377. }
  378. // --- Stateful stereo decode ---
  379. channels := 1
  380. if isWFMStereo {
  381. channels = 2
  382. audio = sess.stereoDecodeStateful(audio, actualDemodRate)
  383. }
  384. // --- Resample towards 48kHz ---
  385. outputRate := actualDemodRate
  386. if actualDemodRate > streamAudioRate {
  387. decim2 := actualDemodRate / streamAudioRate
  388. if decim2 < 1 {
  389. decim2 = 1
  390. }
  391. outputRate = actualDemodRate / decim2
  392. aaTaps := dsp.LowpassFIR(float64(outputRate)/2.0*0.9, actualDemodRate, 63)
  393. if channels > 1 {
  394. nFrames := len(audio) / channels
  395. left := make([]float32, nFrames)
  396. right := make([]float32, nFrames)
  397. for i := 0; i < nFrames; i++ {
  398. left[i] = audio[i*2]
  399. if i*2+1 < len(audio) {
  400. right[i] = audio[i*2+1]
  401. }
  402. }
  403. left = dsp.ApplyFIRReal(left, aaTaps)
  404. right = dsp.ApplyFIRReal(right, aaTaps)
  405. outFrames := nFrames / decim2
  406. if outFrames < 1 {
  407. return nil, 0
  408. }
  409. resampled := make([]float32, outFrames*2)
  410. for i := 0; i < outFrames; i++ {
  411. resampled[i*2] = left[i*decim2]
  412. resampled[i*2+1] = right[i*decim2]
  413. }
  414. audio = resampled
  415. } else {
  416. audio = dsp.ApplyFIRReal(audio, aaTaps)
  417. resampled := make([]float32, 0, len(audio)/decim2+1)
  418. for i := 0; i < len(audio); i += decim2 {
  419. resampled = append(resampled, audio[i])
  420. }
  421. audio = resampled
  422. }
  423. }
  424. // --- De-emphasis (50µs Europe) ---
  425. if isWFM && outputRate > 0 {
  426. const tau = 50e-6
  427. alpha := math.Exp(-1.0 / (float64(outputRate) * tau))
  428. if channels > 1 {
  429. nFrames := len(audio) / channels
  430. yL, yR := sess.deemphL, sess.deemphR
  431. for i := 0; i < nFrames; i++ {
  432. yL = alpha*yL + (1-alpha)*float64(audio[i*2])
  433. audio[i*2] = float32(yL)
  434. yR = alpha*yR + (1-alpha)*float64(audio[i*2+1])
  435. audio[i*2+1] = float32(yR)
  436. }
  437. sess.deemphL, sess.deemphR = yL, yR
  438. } else {
  439. y := sess.deemphL
  440. for i := range audio {
  441. y = alpha*y + (1-alpha)*float64(audio[i])
  442. audio[i] = float32(y)
  443. }
  444. sess.deemphL = y
  445. }
  446. }
  447. return audio, outputRate
  448. }
  449. // stereoDecodeStateful: phase-continuous 38kHz oscillator for L-R extraction.
  450. func (sess *streamSession) stereoDecodeStateful(mono []float32, sampleRate int) []float32 {
  451. if len(mono) == 0 || sampleRate <= 0 {
  452. return nil
  453. }
  454. lp := dsp.LowpassFIR(15000, sampleRate, 101)
  455. lpr := dsp.ApplyFIRReal(mono, lp)
  456. bpHi := dsp.LowpassFIR(53000, sampleRate, 101)
  457. bpLo := dsp.LowpassFIR(23000, sampleRate, 101)
  458. hi := dsp.ApplyFIRReal(mono, bpHi)
  459. lo := dsp.ApplyFIRReal(mono, bpLo)
  460. bpf := make([]float32, len(mono))
  461. for i := range mono {
  462. bpf[i] = hi[i] - lo[i]
  463. }
  464. lr := make([]float32, len(mono))
  465. phase := sess.stereoPhase
  466. inc := 2 * math.Pi * 38000 / float64(sampleRate)
  467. for i := range bpf {
  468. phase += inc
  469. lr[i] = bpf[i] * float32(2*math.Cos(phase))
  470. }
  471. sess.stereoPhase = math.Mod(phase, 2*math.Pi)
  472. lr = dsp.ApplyFIRReal(lr, lp)
  473. out := make([]float32, len(lpr)*2)
  474. for i := range lpr {
  475. out[i*2] = 0.5 * (lpr[i] + lr[i])
  476. out[i*2+1] = 0.5 * (lpr[i] - lr[i])
  477. }
  478. return out
  479. }
  480. // ---------------------------------------------------------------------------
  481. // Session management helpers
  482. // ---------------------------------------------------------------------------
  483. func (st *Streamer) openSession(sig *detector.Signal, now time.Time) (*streamSession, error) {
  484. outputDir := st.policy.OutputDir
  485. if outputDir == "" {
  486. outputDir = "data/recordings"
  487. }
  488. demodName := "NFM"
  489. if sig.Class != nil {
  490. if n := mapClassToDemod(sig.Class.ModType); n != "" {
  491. demodName = n
  492. }
  493. }
  494. channels := 1
  495. if demodName == "WFM_STEREO" {
  496. channels = 2
  497. } else if d := demod.Get(demodName); d != nil {
  498. channels = d.Channels()
  499. }
  500. dirName := fmt.Sprintf("%s_%.0fHz_stream%d",
  501. now.Format("2006-01-02T15-04-05"), sig.CenterHz, sig.ID)
  502. dir := filepath.Join(outputDir, dirName)
  503. if err := os.MkdirAll(dir, 0o755); err != nil {
  504. return nil, err
  505. }
  506. wavPath := filepath.Join(dir, "audio.wav")
  507. f, err := os.Create(wavPath)
  508. if err != nil {
  509. return nil, err
  510. }
  511. if err := writeStreamWAVHeader(f, streamAudioRate, channels); err != nil {
  512. f.Close()
  513. return nil, err
  514. }
  515. sess := &streamSession{
  516. signalID: sig.ID,
  517. centerHz: sig.CenterHz,
  518. bwHz: sig.BWHz,
  519. snrDb: sig.SNRDb,
  520. peakDb: sig.PeakDb,
  521. class: sig.Class,
  522. startTime: now,
  523. lastFeed: now,
  524. dir: dir,
  525. wavFile: f,
  526. wavBuf: bufio.NewWriterSize(f, 64*1024),
  527. sampleRate: streamAudioRate,
  528. channels: channels,
  529. demodName: demodName,
  530. }
  531. log.Printf("STREAM: opened signal=%d %.1fMHz %s dir=%s",
  532. sig.ID, sig.CenterHz/1e6, demodName, dirName)
  533. return sess, nil
  534. }
  535. func closeSession(sess *streamSession, policy *Policy) {
  536. if sess.wavBuf != nil {
  537. _ = sess.wavBuf.Flush()
  538. }
  539. if sess.wavFile != nil {
  540. fixStreamWAVHeader(sess.wavFile, sess.wavSamples, sess.sampleRate, sess.channels)
  541. sess.wavFile.Close()
  542. sess.wavFile = nil
  543. sess.wavBuf = nil
  544. }
  545. dur := sess.lastFeed.Sub(sess.startTime)
  546. files := map[string]any{
  547. "audio": "audio.wav",
  548. "audio_sample_rate": sess.sampleRate,
  549. "audio_channels": sess.channels,
  550. "audio_demod": sess.demodName,
  551. "recording_mode": "streaming",
  552. }
  553. meta := Meta{
  554. EventID: sess.signalID,
  555. Start: sess.startTime,
  556. End: sess.lastFeed,
  557. CenterHz: sess.centerHz,
  558. BandwidthHz: sess.bwHz,
  559. SampleRate: sess.sampleRate,
  560. SNRDb: sess.snrDb,
  561. PeakDb: sess.peakDb,
  562. Class: sess.class,
  563. DurationMs: dur.Milliseconds(),
  564. Files: files,
  565. }
  566. b, err := json.MarshalIndent(meta, "", " ")
  567. if err == nil {
  568. _ = os.WriteFile(filepath.Join(sess.dir, "meta.json"), b, 0o644)
  569. }
  570. if policy != nil {
  571. enforceQuota(policy.OutputDir, policy.MaxDiskMB)
  572. }
  573. }
  574. func appendAudio(sess *streamSession, audio []float32) {
  575. if sess.wavBuf == nil || len(audio) == 0 {
  576. return
  577. }
  578. buf := make([]byte, len(audio)*2)
  579. for i, s := range audio {
  580. v := int16(clip(s * 32767))
  581. binary.LittleEndian.PutUint16(buf[i*2:], uint16(v))
  582. }
  583. n, err := sess.wavBuf.Write(buf)
  584. if err != nil {
  585. log.Printf("STREAM: write error signal=%d: %v", sess.signalID, err)
  586. return
  587. }
  588. sess.wavSamples += int64(n / 2)
  589. }
  590. func (st *Streamer) fanoutAudio(sess *streamSession, audio []float32) {
  591. if len(sess.audioSubs) == 0 {
  592. return
  593. }
  594. pcm := make([]byte, len(audio)*2)
  595. for i, s := range audio {
  596. v := int16(clip(s * 32767))
  597. binary.LittleEndian.PutUint16(pcm[i*2:], uint16(v))
  598. }
  599. alive := sess.audioSubs[:0]
  600. for _, sub := range sess.audioSubs {
  601. select {
  602. case sub.ch <- pcm:
  603. default:
  604. }
  605. alive = append(alive, sub)
  606. }
  607. sess.audioSubs = alive
  608. }
  609. func (st *Streamer) classAllowed(cls *classifier.Classification) bool {
  610. if len(st.policy.ClassFilter) == 0 {
  611. return true
  612. }
  613. if cls == nil {
  614. return false
  615. }
  616. for _, f := range st.policy.ClassFilter {
  617. if strings.EqualFold(f, string(cls.ModType)) {
  618. return true
  619. }
  620. }
  621. return false
  622. }
  623. // ---------------------------------------------------------------------------
  624. // WAV header helpers
  625. // ---------------------------------------------------------------------------
  626. func writeStreamWAVHeader(f *os.File, sampleRate int, channels int) error {
  627. if channels <= 0 {
  628. channels = 1
  629. }
  630. hdr := make([]byte, 44)
  631. copy(hdr[0:4], "RIFF")
  632. binary.LittleEndian.PutUint32(hdr[4:8], 36)
  633. copy(hdr[8:12], "WAVE")
  634. copy(hdr[12:16], "fmt ")
  635. binary.LittleEndian.PutUint32(hdr[16:20], 16)
  636. binary.LittleEndian.PutUint16(hdr[20:22], 1)
  637. binary.LittleEndian.PutUint16(hdr[22:24], uint16(channels))
  638. binary.LittleEndian.PutUint32(hdr[24:28], uint32(sampleRate))
  639. binary.LittleEndian.PutUint32(hdr[28:32], uint32(sampleRate*channels*2))
  640. binary.LittleEndian.PutUint16(hdr[32:34], uint16(channels*2))
  641. binary.LittleEndian.PutUint16(hdr[34:36], 16)
  642. copy(hdr[36:40], "data")
  643. binary.LittleEndian.PutUint32(hdr[40:44], 0)
  644. _, err := f.Write(hdr)
  645. return err
  646. }
  647. func fixStreamWAVHeader(f *os.File, totalSamples int64, sampleRate int, channels int) {
  648. dataSize := uint32(totalSamples * 2)
  649. var buf [4]byte
  650. binary.LittleEndian.PutUint32(buf[:], 36+dataSize)
  651. if _, err := f.Seek(4, 0); err != nil {
  652. return
  653. }
  654. _, _ = f.Write(buf[:])
  655. binary.LittleEndian.PutUint32(buf[:], uint32(sampleRate))
  656. if _, err := f.Seek(24, 0); err != nil {
  657. return
  658. }
  659. _, _ = f.Write(buf[:])
  660. binary.LittleEndian.PutUint32(buf[:], uint32(sampleRate*channels*2))
  661. if _, err := f.Seek(28, 0); err != nil {
  662. return
  663. }
  664. _, _ = f.Write(buf[:])
  665. binary.LittleEndian.PutUint32(buf[:], dataSize)
  666. if _, err := f.Seek(40, 0); err != nil {
  667. return
  668. }
  669. _, _ = f.Write(buf[:])
  670. }