This commit is contained in:
2026-06-13 19:14:24 +02:00
parent 0b3e22c97e
commit 81e505e040
19 changed files with 194 additions and 56 deletions
+22 -6
View File
@@ -221,27 +221,43 @@ func (r *Recorder) RestartQSO() {
r.active = true
}
// SaveQSO writes the accumulated recording to path as a WAV and stops
// accumulating. Returns an error if no recording was active.
func (r *Recorder) SaveQSO(path string) error {
// TakeQSO snapshots the accumulated recording as raw 16 kHz mono PCM bytes and
// stops accumulating — fast, no encoding. The next BeginQSO can safely start a
// new take immediately. Pair with WritePCM to encode/write off the hot path so
// a long recording doesn't delay logging.
func (r *Recorder) TakeQSO() ([]byte, error) {
r.mu.Lock()
if !r.active {
r.mu.Unlock()
return fmt.Errorf("no active recording")
return nil, fmt.Errorf("no active recording")
}
samples := r.acc
r.acc, r.active = nil, false
r.mu.Unlock()
if len(samples) == 0 {
return fmt.Errorf("recording was empty")
return nil, fmt.Errorf("recording was empty")
}
data := int16sToBytes(samples)
return int16sToBytes(samples), nil
}
// WritePCM encodes raw 16 kHz mono PCM to path (WAV, or MP3 when path ends in
// .mp3). Slow for MP3 — call off the logging path.
func WritePCM(path string, data []byte) error {
if strings.HasSuffix(strings.ToLower(path), ".mp3") {
return writeMP3(path, data)
}
return writeWAV(path, data)
}
// SaveQSO snapshots and writes the recording in one call (synchronous).
func (r *Recorder) SaveQSO(path string) error {
data, err := r.TakeQSO()
if err != nil {
return err
}
return WritePCM(path, data)
}
// DiscardQSO drops the active accumulation without saving (callsign cleared).
func (r *Recorder) DiscardQSO() {
r.mu.Lock()
+6
View File
@@ -16,6 +16,7 @@ type Config struct {
User string
Password string
From string
ReplyTo string // optional Reply-To: where replies should go (e.g. a personal inbox)
Encryption string // "ssl" | "starttls" | "none"
Auth bool // SMTP requires authorization (send username/password)
}
@@ -57,6 +58,11 @@ func Send(cfg Config, to, subject, body, attachPath string) error {
if err := m.To(to); err != nil {
return fmt.Errorf("bad recipient %q: %w", to, err)
}
if cfg.ReplyTo != "" {
if err := m.ReplyTo(cfg.ReplyTo); err != nil {
return fmt.Errorf("bad reply-to %q: %w", cfg.ReplyTo, err)
}
}
m.Subject(subject)
m.SetBodyString(mail.TypeTextPlain, body)
if attachPath != "" {
+14 -4
View File
@@ -255,7 +255,7 @@ func (HeuristicEngine) Propose(photos []PhotoAnalysis, profile ProfileInfo) ([]T
cool: sorted[0].Warmth < 0.02,
}
if len(sorted) > 1 {
plan.inserts = sorted[1:min(len(sorted), 3)] // hero + up to 2 inserts
plan.inserts = sorted[1:min(len(sorted), 5)] // hero + up to 4 inserts
}
// Only side-column inserts (or none): a bottom strip collides with the QSO
@@ -671,9 +671,19 @@ func placeInserts(inserts []PhotoAnalysis, hero PhotoAnalysis, archetype string)
return els, pxRect{x: margin, y: cardH - 380, w: cardW - 2*margin, h: 380 - margin}
}
// Side column: pick the calmer half of the hero photo.
n := min(len(inserts), 3)
w, gap, margin := 400.0, 20.0, 70.0
// Side column: pick the calmer half of the hero photo. The insert width
// shrinks as needed so up to 4 framed photos stack down the side.
n := min(len(inserts), 4)
gap, margin := 20.0, 70.0
availH := float64(cardH) - 2*margin - float64(n-1)*gap
var arSum float64
for i := 0; i < n; i++ {
arSum += float64(inserts[i].H) / float64(inserts[i].W)
}
w := 400.0
if arSum > 0 {
w = clamp(availH/arSum, 240, 400) // fit all n vertically, within sane bounds
}
left := halfDetail(hero, true) < halfDetail(hero, false)
x := cardW - w - margin
if left {
+2
View File
@@ -106,6 +106,8 @@ type FxParams struct {
Grunge *float64 `json:"grunge,omitempty"`
Bevel *float64 `json:"bevel,omitempty"`
Seed *float64 `json:"seed,omitempty"`
Dark string `json:"dark,omitempty"` // dark inter-letter edge (per colour palette)
Outer string `json:"outer,omitempty"` // silver/rim colour (glossy)
}
// setKeys lists the JSON names of the params that are actually set, for