up
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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 != "" {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user