aduio mail
This commit is contained in:
@@ -31,6 +31,8 @@ type Recorder struct {
|
||||
bufA []int16 // From Radio
|
||||
bufB []int16 // mic
|
||||
twoSrc bool
|
||||
gainA float64 // From Radio gain (1.0 = unity), guarded by srcMu
|
||||
gainB float64 // mic gain
|
||||
|
||||
// Mixed output state (guarded by mu).
|
||||
ring []int16 // last prerollSamples of mixed audio
|
||||
@@ -38,7 +40,36 @@ type Recorder struct {
|
||||
acc []int16 // active QSO accumulation (seeded from ring on BeginQSO)
|
||||
}
|
||||
|
||||
func NewRecorder() *Recorder { return &Recorder{} }
|
||||
func NewRecorder() *Recorder { return &Recorder{gainA: 1, gainB: 1} }
|
||||
|
||||
// SetGains sets the per-source mix levels (1.0 = unity). Use this to balance a
|
||||
// hot mic against quieter rig RX audio. Values ≤0 fall back to unity.
|
||||
func (r *Recorder) SetGains(fromGain, micGain float64) {
|
||||
if fromGain <= 0 {
|
||||
fromGain = 1
|
||||
}
|
||||
if micGain <= 0 {
|
||||
micGain = 1
|
||||
}
|
||||
r.srcMu.Lock()
|
||||
r.gainA, r.gainB = fromGain, micGain
|
||||
r.srcMu.Unlock()
|
||||
}
|
||||
|
||||
// scaleSample applies gain to a sample with clamping.
|
||||
func scaleSample(s int16, g float64) int16 {
|
||||
if g == 1 {
|
||||
return s
|
||||
}
|
||||
v := float64(s) * g
|
||||
if v > 32767 {
|
||||
return 32767
|
||||
}
|
||||
if v < -32768 {
|
||||
return -32768
|
||||
}
|
||||
return int16(v)
|
||||
}
|
||||
|
||||
func (r *Recorder) Running() bool {
|
||||
r.mu.Lock()
|
||||
@@ -128,7 +159,7 @@ func (r *Recorder) mixTick() {
|
||||
if n > 0 {
|
||||
mixed = make([]int16, n)
|
||||
for i := 0; i < n; i++ {
|
||||
mixed[i] = clampSum(r.bufA[i], r.bufB[i])
|
||||
mixed[i] = clampSum(scaleSample(r.bufA[i], r.gainA), scaleSample(r.bufB[i], r.gainB))
|
||||
}
|
||||
r.bufA = append(r.bufA[:0], r.bufA[n:]...)
|
||||
r.bufB = append(r.bufB[:0], r.bufB[n:]...)
|
||||
@@ -142,7 +173,9 @@ func (r *Recorder) mixTick() {
|
||||
}
|
||||
} else if len(r.bufA) > 0 {
|
||||
mixed = make([]int16, len(r.bufA))
|
||||
copy(mixed, r.bufA)
|
||||
for i, s := range r.bufA {
|
||||
mixed[i] = scaleSample(s, r.gainA)
|
||||
}
|
||||
r.bufA = r.bufA[:0]
|
||||
}
|
||||
r.srcMu.Unlock()
|
||||
|
||||
+25
-31
@@ -202,39 +202,33 @@ func (o *OmniRig) SetFrequency(hz int64) error {
|
||||
debugLog.Printf("OmniRig.SetFrequency(%d Hz / %.6f MHz): rig=%q status=%d(%s) vfo=%q(raw=%d) split=%d writeableParams=0x%X",
|
||||
hz, float64(hz)/1e6, rigType, status, statusStr, vfo, rawVfo, split, writeable)
|
||||
|
||||
// Pick the active VFO's specific property. Many rig .ini files only define
|
||||
// a WRITE command for FreqA/FreqB but not the generic Freq.
|
||||
prop := "FreqA"
|
||||
switch vfo {
|
||||
case "B", "BB", "BA":
|
||||
prop = "FreqB"
|
||||
case "A", "AA", "AB":
|
||||
prop = "FreqA"
|
||||
}
|
||||
|
||||
wroteOK := false
|
||||
if _, err := oleutil.PutProperty(o.rig, prop, hz32); err != nil {
|
||||
debugLog.Printf("OmniRig.SetFrequency: PutProperty(%s) error: %v", prop, err)
|
||||
// Primary path: OmniRig's SetSimplexMode is the rig-agnostic "QSY here"
|
||||
// method (RX=TX=freq, simplex). It works on rigs — notably Icom (IC-9100) —
|
||||
// where direct FreqA/FreqB writes are accepted but never move the radio.
|
||||
// Clearing split is the right thing when tuning to a spot anyway.
|
||||
if _, err := oleutil.CallMethod(o.rig, "SetSimplexMode", int32(hz32)); err == nil {
|
||||
debugLog.Printf("OmniRig.SetFrequency: SetSimplexMode(%d) OK", hz32)
|
||||
} else {
|
||||
debugLog.Printf("OmniRig.SetFrequency: PutProperty(%s, %d) OK", prop, hz32)
|
||||
wroteOK = true
|
||||
}
|
||||
|
||||
// Belt-and-suspenders: when NOT in split, also write the generic Freq.
|
||||
// Icom .ini files commonly honour Freq (CI-V "set operating frequency")
|
||||
// but ignore FreqA/FreqB, so the rig changed mode but never moved — this
|
||||
// is exactly the IC-9100 "mode changes, freq doesn't" symptom.
|
||||
if split == 0 {
|
||||
if _, err := oleutil.PutProperty(o.rig, "Freq", hz32); err != nil {
|
||||
debugLog.Printf("OmniRig.SetFrequency: PutProperty(Freq) error: %v", err)
|
||||
if !wroteOK {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
debugLog.Printf("OmniRig.SetFrequency: PutProperty(Freq, %d) OK", hz32)
|
||||
debugLog.Printf("OmniRig.SetFrequency: SetSimplexMode unavailable (%v) — using property writes", err)
|
||||
// Fallback: write the active VFO's property AND the generic Freq
|
||||
// (always — some .ini honour only one, and split here is often misread).
|
||||
prop := "FreqA"
|
||||
switch vfo {
|
||||
case "B", "BB", "BA":
|
||||
prop = "FreqB"
|
||||
}
|
||||
okAny := false
|
||||
for _, p := range []string{prop, "Freq"} {
|
||||
if _, e := oleutil.PutProperty(o.rig, p, hz32); e != nil {
|
||||
debugLog.Printf("OmniRig.SetFrequency: PutProperty(%s) error: %v", p, e)
|
||||
} else {
|
||||
debugLog.Printf("OmniRig.SetFrequency: PutProperty(%s, %d) OK", p, hz32)
|
||||
okAny = true
|
||||
}
|
||||
}
|
||||
if !okAny {
|
||||
return fmt.Errorf("OmniRig: no writable frequency property for this rig")
|
||||
}
|
||||
} else if !wroteOK {
|
||||
return fmt.Errorf("OmniRig: could not write %s and split is on (won't touch generic Freq)", prop)
|
||||
}
|
||||
|
||||
// Read back all three immediately. OmniRig is async (the CAT command is
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
// Package email sends QSO recordings to correspondents via SMTP. Pure Go (no
|
||||
// CGO) using go-mail; supports implicit SSL (465), STARTTLS (587) or none.
|
||||
package email
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/wneessen/go-mail"
|
||||
)
|
||||
|
||||
// Config is the user's SMTP configuration.
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
User string
|
||||
Password string
|
||||
From string
|
||||
Encryption string // "ssl" | "starttls" | "none"
|
||||
Auth bool // SMTP requires authorization (send username/password)
|
||||
}
|
||||
|
||||
func (c Config) opts() []mail.Option {
|
||||
o := []mail.Option{mail.WithPort(c.Port), mail.WithTimeout(30 * time.Second)}
|
||||
if c.Auth && c.User != "" {
|
||||
// AutoDiscover negotiates whatever mechanism the server advertises
|
||||
// (LOGIN, PLAIN, CRAM-MD5, …). OVH, for instance, rejects forced PLAIN.
|
||||
o = append(o, mail.WithSMTPAuth(mail.SMTPAuthAutoDiscover), mail.WithUsername(c.User), mail.WithPassword(c.Password))
|
||||
}
|
||||
switch c.Encryption {
|
||||
case "ssl":
|
||||
o = append(o, mail.WithSSL())
|
||||
case "none":
|
||||
o = append(o, mail.WithTLSPolicy(mail.NoTLS))
|
||||
default: // starttls
|
||||
o = append(o, mail.WithTLSPolicy(mail.TLSMandatory))
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
// Send delivers a plain-text email to `to`, optionally attaching a file.
|
||||
func Send(cfg Config, to, subject, body, attachPath string) error {
|
||||
if cfg.Host == "" {
|
||||
return fmt.Errorf("SMTP server not configured")
|
||||
}
|
||||
if to == "" {
|
||||
return fmt.Errorf("no recipient e-mail")
|
||||
}
|
||||
from := cfg.From
|
||||
if from == "" {
|
||||
from = cfg.User
|
||||
}
|
||||
m := mail.NewMsg()
|
||||
if err := m.From(from); err != nil {
|
||||
return fmt.Errorf("bad sender %q: %w", from, err)
|
||||
}
|
||||
if err := m.To(to); err != nil {
|
||||
return fmt.Errorf("bad recipient %q: %w", to, err)
|
||||
}
|
||||
m.Subject(subject)
|
||||
m.SetBodyString(mail.TypeTextPlain, body)
|
||||
if attachPath != "" {
|
||||
m.AttachFile(attachPath)
|
||||
}
|
||||
client, err := mail.NewClient(cfg.Host, cfg.opts()...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("smtp client: %w", err)
|
||||
}
|
||||
if err := client.DialAndSend(m); err != nil {
|
||||
return fmt.Errorf("send: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user