package main import ( "context" "database/sql" "encoding/binary" "encoding/json" "fmt" "math" "math/rand" "net/http" "os" "os/signal" "reflect" "strings" "syscall" "time" _ "github.com/mattn/go-sqlite3" "github.com/mdp/qrterminal" "bytes" "go.mau.fi/whatsmeow" waProto "go.mau.fi/whatsmeow/binary/proto" "go.mau.fi/whatsmeow/store/sqlstore" "go.mau.fi/whatsmeow/types" "go.mau.fi/whatsmeow/types/events" waLog "go.mau.fi/whatsmeow/util/log" "google.golang.org/protobuf/proto" ) // Message represents a chat message for our client type Message struct { Time time.Time Sender string Content string IsFromMe bool } // Database handler for storing message history type MessageStore struct { db *sql.DB } // Initialize message store func NewMessageStore() (*MessageStore, error) { // Create directory for database if it doesn't exist if err := os.MkdirAll("store", 0755); err != nil { return nil, fmt.Errorf("failed to create store directory: %v", err) } // Open SQLite database for messages db, err := sql.Open("sqlite3", "file:store/messages.db?_foreign_keys=on") if err != nil { return nil, fmt.Errorf("failed to open message database: %v", err) } // Create tables if they don't exist _, err = db.Exec(` CREATE TABLE IF NOT EXISTS chats ( jid TEXT PRIMARY KEY, name TEXT, last_message_time TIMESTAMP ); CREATE TABLE IF NOT EXISTS messages ( id TEXT, chat_jid TEXT, sender TEXT, content TEXT, timestamp TIMESTAMP, is_from_me BOOLEAN, PRIMARY KEY (id, chat_jid), FOREIGN KEY (chat_jid) REFERENCES chats(jid) ); `) if err != nil { db.Close() return nil, fmt.Errorf("failed to create tables: %v", err) } return &MessageStore{db: db}, nil } // Close the database connection func (store *MessageStore) Close() error { return store.db.Close() } // Store a chat in the database func (store *MessageStore) StoreChat(jid, name string, lastMessageTime time.Time) error { _, err := store.db.Exec( "INSERT OR REPLACE INTO chats (jid, name, last_message_time) VALUES (?, ?, ?)", jid, name, lastMessageTime, ) return err } // Store a message in the database func (store *MessageStore) StoreMessage(id, chatJID, sender, content string, timestamp time.Time, isFromMe bool) error { // Only store if there's actual content if content == "" { return nil } _, err := store.db.Exec( "INSERT OR REPLACE INTO messages (id, chat_jid, sender, content, timestamp, is_from_me) VALUES (?, ?, ?, ?, ?, ?)", id, chatJID, sender, content, timestamp, isFromMe, ) return err } // Get messages from a chat func (store *MessageStore) GetMessages(chatJID string, limit int) ([]Message, error) { rows, err := store.db.Query( "SELECT sender, content, timestamp, is_from_me FROM messages WHERE chat_jid = ? ORDER BY timestamp DESC LIMIT ?", chatJID, limit, ) if err != nil { return nil, err } defer rows.Close() var messages []Message for rows.Next() { var msg Message var timestamp time.Time err := rows.Scan(&msg.Sender, &msg.Content, ×tamp, &msg.IsFromMe) if err != nil { return nil, err } msg.Time = timestamp messages = append(messages, msg) } return messages, nil } // Get all chats func (store *MessageStore) GetChats() (map[string]time.Time, error) { rows, err := store.db.Query("SELECT jid, last_message_time FROM chats ORDER BY last_message_time DESC") if err != nil { return nil, err } defer rows.Close() chats := make(map[string]time.Time) for rows.Next() { var jid string var lastMessageTime time.Time err := rows.Scan(&jid, &lastMessageTime) if err != nil { return nil, err } chats[jid] = lastMessageTime } return chats, nil } // Extract text content from a message func extractTextContent(msg *waProto.Message) string { if msg == nil { return "" } // Try to get text content if text := msg.GetConversation(); text != "" { return text } else if extendedText := msg.GetExtendedTextMessage(); extendedText != nil { return extendedText.GetText() } // For now, we're ignoring non-text messages return "" } // SendMessageResponse represents the response for the send message API type SendMessageResponse struct { Success bool `json:"success"` Message string `json:"message"` } // SendMessageRequest represents the request body for the send message API type SendMessageRequest struct { Recipient string `json:"recipient"` Message string `json:"message"` MediaPath string `json:"media_path,omitempty"` } // Function to send a WhatsApp message func sendWhatsAppMessage(client *whatsmeow.Client, recipient string, message string, mediaPath string) (bool, string) { if !client.IsConnected() { return false, "Not connected to WhatsApp" } // Create JID for recipient var recipientJID types.JID var err error // Check if recipient is a JID isJID := strings.Contains(recipient, "@") if isJID { // Parse the JID string recipientJID, err = types.ParseJID(recipient) if err != nil { return false, fmt.Sprintf("Error parsing JID: %v", err) } } else { // Create JID from phone number recipientJID = types.JID{ User: recipient, Server: "s.whatsapp.net", // For personal chats } } msg := &waProto.Message{} // Check if we have media to send if mediaPath != "" { // Read media file mediaData, err := os.ReadFile(mediaPath) if err != nil { return false, fmt.Sprintf("Error reading media file: %v", err) } // Determine media type and mime type based on file extension fileExt := strings.ToLower(mediaPath[strings.LastIndex(mediaPath, ".")+1:]) var mediaType whatsmeow.MediaType var mimeType string // Handle different media types switch fileExt { // Image types case "jpg", "jpeg": mediaType = whatsmeow.MediaImage mimeType = "image/jpeg" case "png": mediaType = whatsmeow.MediaImage mimeType = "image/png" case "gif": mediaType = whatsmeow.MediaImage mimeType = "image/gif" case "webp": mediaType = whatsmeow.MediaImage mimeType = "image/webp" // Audio types case "ogg": mediaType = whatsmeow.MediaAudio mimeType = "audio/ogg; codecs=opus" // Video types case "mp4": mediaType = whatsmeow.MediaVideo mimeType = "video/mp4" case "avi": mediaType = whatsmeow.MediaVideo mimeType = "video/avi" case "mov": mediaType = whatsmeow.MediaVideo mimeType = "video/quicktime" // Document types (for any other file type) default: mediaType = whatsmeow.MediaDocument mimeType = "application/octet-stream" } // Upload media to WhatsApp servers resp, err := client.Upload(context.Background(), mediaData, mediaType) if err != nil { return false, fmt.Sprintf("Error uploading media: %v", err) } fmt.Println("Media uploaded", resp) // Create the appropriate message type based on media type switch mediaType { case whatsmeow.MediaImage: msg.ImageMessage = &waProto.ImageMessage{ Caption: proto.String(message), Mimetype: proto.String(mimeType), URL: &resp.URL, DirectPath: &resp.DirectPath, MediaKey: resp.MediaKey, FileEncSHA256: resp.FileEncSHA256, FileSHA256: resp.FileSHA256, FileLength: &resp.FileLength, } case whatsmeow.MediaAudio: // Handle ogg audio files var seconds uint32 = 30 // Default fallback var waveform []byte = nil // Try to analyze the ogg file if strings.Contains(mimeType, "ogg") { analyzedSeconds, analyzedWaveform, err := analyzeOggOpus(mediaData) if err == nil { seconds = analyzedSeconds waveform = analyzedWaveform } else { return false, fmt.Sprintf("Failed to analyze Ogg Opus file: %v", err) } } else { fmt.Printf("Not an Ogg Opus file: %s\n", mimeType) } msg.AudioMessage = &waProto.AudioMessage{ Mimetype: proto.String(mimeType), URL: &resp.URL, DirectPath: &resp.DirectPath, MediaKey: resp.MediaKey, FileEncSHA256: resp.FileEncSHA256, FileSHA256: resp.FileSHA256, FileLength: &resp.FileLength, Seconds: proto.Uint32(seconds), PTT: proto.Bool(true), Waveform: waveform, } case whatsmeow.MediaVideo: msg.VideoMessage = &waProto.VideoMessage{ Caption: proto.String(message), Mimetype: proto.String(mimeType), URL: &resp.URL, DirectPath: &resp.DirectPath, MediaKey: resp.MediaKey, FileEncSHA256: resp.FileEncSHA256, FileSHA256: resp.FileSHA256, FileLength: &resp.FileLength, } case whatsmeow.MediaDocument: msg.DocumentMessage = &waProto.DocumentMessage{ Title: proto.String(mediaPath[strings.LastIndex(mediaPath, "/")+1:]), Caption: proto.String(message), Mimetype: proto.String(mimeType), URL: &resp.URL, DirectPath: &resp.DirectPath, MediaKey: resp.MediaKey, FileEncSHA256: resp.FileEncSHA256, FileSHA256: resp.FileSHA256, FileLength: &resp.FileLength, } } } else { msg.Conversation = proto.String(message) } // Send message _, err = client.SendMessage(context.Background(), recipientJID, msg) if err != nil { return false, fmt.Sprintf("Error sending message: %v", err) } return true, fmt.Sprintf("Message sent to %s", recipient) } // Start a REST API server to expose the WhatsApp client functionality func startRESTServer(client *whatsmeow.Client, port int) { // Handler for sending messages http.HandleFunc("/api/send", func(w http.ResponseWriter, r *http.Request) { // Only allow POST requests if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } // Parse the request body var req SendMessageRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request format", http.StatusBadRequest) return } // Validate request if req.Recipient == "" { http.Error(w, "Recipient is required", http.StatusBadRequest) return } if req.Message == "" && req.MediaPath == "" { http.Error(w, "Message or media path is required", http.StatusBadRequest) return } fmt.Println("Received request to send message", req.Message, req.MediaPath) // Send the message success, message := sendWhatsAppMessage(client, req.Recipient, req.Message, req.MediaPath) fmt.Println("Message sent", success, message) // Set response headers w.Header().Set("Content-Type", "application/json") // Set appropriate status code if !success { w.WriteHeader(http.StatusInternalServerError) } // Send response json.NewEncoder(w).Encode(SendMessageResponse{ Success: success, Message: message, }) }) // Start the server serverAddr := fmt.Sprintf(":%d", port) fmt.Printf("Starting REST API server on %s...\n", serverAddr) // Run server in a goroutine so it doesn't block go func() { if err := http.ListenAndServe(serverAddr, nil); err != nil { fmt.Printf("REST API server error: %v\n", err) } }() } func main() { // Set up logger logger := waLog.Stdout("Client", "INFO", true) logger.Infof("Starting WhatsApp client...") // Create database connection for storing session data dbLog := waLog.Stdout("Database", "INFO", true) // Create directory for database if it doesn't exist if err := os.MkdirAll("store", 0755); err != nil { logger.Errorf("Failed to create store directory: %v", err) return } container, err := sqlstore.New("sqlite3", "file:store/whatsapp.db?_foreign_keys=on", dbLog) if err != nil { logger.Errorf("Failed to connect to database: %v", err) return } // Get device store - This contains session information deviceStore, err := container.GetFirstDevice() if err != nil { if err == sql.ErrNoRows { // No device exists, create one deviceStore = container.NewDevice() logger.Infof("Created new device") } else { logger.Errorf("Failed to get device: %v", err) return } } // Create client instance client := whatsmeow.NewClient(deviceStore, logger) if client == nil { logger.Errorf("Failed to create WhatsApp client") return } // Initialize message store messageStore, err := NewMessageStore() if err != nil { logger.Errorf("Failed to initialize message store: %v", err) return } defer messageStore.Close() // Setup event handling for messages and history sync client.AddEventHandler(func(evt interface{}) { switch v := evt.(type) { case *events.Message: // Process regular messages handleMessage(client, messageStore, v, logger) case *events.HistorySync: // Process history sync events handleHistorySync(client, messageStore, v, logger) case *events.Connected: logger.Infof("Connected to WhatsApp") case *events.LoggedOut: logger.Warnf("Device logged out, please scan QR code to log in again") } }) // Create channel to track connection success connected := make(chan bool, 1) // Connect to WhatsApp if client.Store.ID == nil { // No ID stored, this is a new client, need to pair with phone qrChan, _ := client.GetQRChannel(context.Background()) err = client.Connect() if err != nil { logger.Errorf("Failed to connect: %v", err) return } // Print QR code for pairing with phone for evt := range qrChan { if evt.Event == "code" { fmt.Println("\nScan this QR code with your WhatsApp app:") qrterminal.GenerateHalfBlock(evt.Code, qrterminal.L, os.Stdout) } else if evt.Event == "success" { connected <- true break } } // Wait for connection select { case <-connected: fmt.Println("\nSuccessfully connected and authenticated!") case <-time.After(3 * time.Minute): logger.Errorf("Timeout waiting for QR code scan") return } } else { // Already logged in, just connect err = client.Connect() if err != nil { logger.Errorf("Failed to connect: %v", err) return } connected <- true } // Wait a moment for connection to stabilize time.Sleep(2 * time.Second) if !client.IsConnected() { logger.Errorf("Failed to establish stable connection") return } fmt.Println("\nāœ“ Connected to WhatsApp! Type 'help' for commands.") // Start REST API server startRESTServer(client, 8080) // Create a channel to keep the main goroutine alive exitChan := make(chan os.Signal, 1) signal.Notify(exitChan, syscall.SIGINT, syscall.SIGTERM) fmt.Println("REST server is running. Press Ctrl+C to disconnect and exit.") // Wait for termination signal <-exitChan fmt.Println("Disconnecting...") // Disconnect client client.Disconnect() } // GetChatName determines the appropriate name for a chat based on JID and other info func GetChatName(client *whatsmeow.Client, messageStore *MessageStore, jid types.JID, chatJID string, conversation interface{}, sender string, logger waLog.Logger) string { // First, check if chat already exists in database with a name var existingName string err := messageStore.db.QueryRow("SELECT name FROM chats WHERE jid = ?", chatJID).Scan(&existingName) if err == nil && existingName != "" { // Chat exists with a name, use that logger.Infof("Using existing chat name for %s: %s", chatJID, existingName) return existingName } // Need to determine chat name var name string if jid.Server == "g.us" { // This is a group chat logger.Infof("Getting name for group: %s", chatJID) // Use conversation data if provided (from history sync) if conversation != nil { // Extract name from conversation if available // This uses type assertions to handle different possible types var displayName, convName *string // Try to extract the fields we care about regardless of the exact type v := reflect.ValueOf(conversation) if v.Kind() == reflect.Ptr && !v.IsNil() { v = v.Elem() // Try to find DisplayName field if displayNameField := v.FieldByName("DisplayName"); displayNameField.IsValid() && displayNameField.Kind() == reflect.Ptr && !displayNameField.IsNil() { dn := displayNameField.Elem().String() displayName = &dn } // Try to find Name field if nameField := v.FieldByName("Name"); nameField.IsValid() && nameField.Kind() == reflect.Ptr && !nameField.IsNil() { n := nameField.Elem().String() convName = &n } } // Use the name we found if displayName != nil && *displayName != "" { name = *displayName } else if convName != nil && *convName != "" { name = *convName } } // If we didn't get a name, try group info if name == "" { groupInfo, err := client.GetGroupInfo(jid) if err == nil && groupInfo.Name != "" { name = groupInfo.Name } else { // Fallback name for groups name = fmt.Sprintf("Group %s", jid.User) } } logger.Infof("Using group name: %s", name) } else { // This is an individual contact logger.Infof("Getting name for contact: %s", chatJID) // Just use contact info (full name) contact, err := client.Store.Contacts.GetContact(jid) if err == nil && contact.FullName != "" { name = contact.FullName } else if sender != "" { // Fallback to sender name = sender } else { // Last fallback to JID name = jid.User } logger.Infof("Using contact name: %s", name) } return name } // Handle regular incoming messages func handleMessage(client *whatsmeow.Client, messageStore *MessageStore, msg *events.Message, logger waLog.Logger) { // Extract text content content := extractTextContent(msg.Message) if content == "" { return // Skip non-text messages } // Save message to database chatJID := msg.Info.Chat.String() sender := msg.Info.Sender.User // Get appropriate chat name (pass nil for conversation since we don't have one for regular messages) name := GetChatName(client, messageStore, msg.Info.Chat, chatJID, nil, sender, logger) // Update chat in database with the message timestamp (keeps last message time updated) err := messageStore.StoreChat(chatJID, name, msg.Info.Timestamp) if err != nil { logger.Warnf("Failed to store chat: %v", err) } // Store message in database err = messageStore.StoreMessage( msg.Info.ID, chatJID, sender, content, msg.Info.Timestamp, msg.Info.IsFromMe, ) if err != nil { logger.Warnf("Failed to store message: %v", err) } else { // Log message reception timestamp := msg.Info.Timestamp.Format("2006-01-02 15:04:05") direction := "←" if msg.Info.IsFromMe { direction = "→" } fmt.Printf("[%s] %s %s: %s\n", timestamp, direction, sender, content) } } // Handle history sync events func handleHistorySync(client *whatsmeow.Client, messageStore *MessageStore, historySync *events.HistorySync, logger waLog.Logger) { fmt.Printf("Received history sync event with %d conversations\n", len(historySync.Data.Conversations)) syncedCount := 0 for _, conversation := range historySync.Data.Conversations { // Parse JID from the conversation if conversation.ID == nil { continue } chatJID := *conversation.ID // Try to parse the JID jid, err := types.ParseJID(chatJID) if err != nil { logger.Warnf("Failed to parse JID %s: %v", chatJID, err) continue } // Get appropriate chat name by passing the history sync conversation directly name := GetChatName(client, messageStore, jid, chatJID, conversation, "", logger) // Process messages messages := conversation.Messages if len(messages) > 0 { // Update chat with latest message timestamp latestMsg := messages[0] if latestMsg == nil || latestMsg.Message == nil { continue } // Get timestamp from message info timestamp := time.Time{} if ts := latestMsg.Message.GetMessageTimestamp(); ts != 0 { timestamp = time.Unix(int64(ts), 0) } else { continue } messageStore.StoreChat(chatJID, name, timestamp) // Store messages for _, msg := range messages { if msg == nil || msg.Message == nil { continue } // Extract text content var content string if msg.Message.Message != nil { if conv := msg.Message.Message.GetConversation(); conv != "" { content = conv } else if ext := msg.Message.Message.GetExtendedTextMessage(); ext != nil { content = ext.GetText() } } // Log the message content for debugging logger.Infof("Message content: %v", content) // Skip non-text messages if content == "" { continue } // Determine sender var sender string isFromMe := false if msg.Message.Key != nil { if msg.Message.Key.FromMe != nil { isFromMe = *msg.Message.Key.FromMe } if !isFromMe && msg.Message.Key.Participant != nil && *msg.Message.Key.Participant != "" { sender = *msg.Message.Key.Participant } else if isFromMe { sender = client.Store.ID.User } else { sender = jid.User } } else { sender = jid.User } // Store message msgID := "" if msg.Message.Key != nil && msg.Message.Key.ID != nil { msgID = *msg.Message.Key.ID } // Get message timestamp timestamp := time.Time{} if ts := msg.Message.GetMessageTimestamp(); ts != 0 { timestamp = time.Unix(int64(ts), 0) } else { continue } err = messageStore.StoreMessage( msgID, chatJID, sender, content, timestamp, isFromMe, ) if err != nil { logger.Warnf("Failed to store history message: %v", err) } else { syncedCount++ // Log successful message storage logger.Infof("Stored message: [%s] %s -> %s: %s", timestamp.Format("2006-01-02 15:04:05"), sender, chatJID, content) } } } } fmt.Printf("History sync complete. Stored %d text messages.\n", syncedCount) } // Request history sync from the server func requestHistorySync(client *whatsmeow.Client) { if client == nil { fmt.Println("Client is not initialized. Cannot request history sync.") return } if !client.IsConnected() { fmt.Println("Client is not connected. Please ensure you are connected to WhatsApp first.") return } if client.Store.ID == nil { fmt.Println("Client is not logged in. Please scan the QR code first.") return } // Build and send a history sync request historyMsg := client.BuildHistorySyncRequest(nil, 100) if historyMsg == nil { fmt.Println("Failed to build history sync request.") return } _, err := client.SendMessage(context.Background(), types.JID{ Server: "s.whatsapp.net", User: "status", }, historyMsg) if err != nil { fmt.Printf("Failed to request history sync: %v\n", err) } else { fmt.Println("History sync requested. Waiting for server response...") } } // analyzeOggOpus tries to extract duration and generate a simple waveform from an Ogg Opus file func analyzeOggOpus(data []byte) (duration uint32, waveform []byte, err error) { // Try to detect if this is a valid Ogg file by checking for the "OggS" signature // at the beginning of the file if len(data) < 4 || string(data[0:4]) != "OggS" { return 0, nil, fmt.Errorf("not a valid Ogg file (missing OggS signature)") } // Parse Ogg pages to find the last page with a valid granule position var lastGranule uint64 var sampleRate uint32 = 48000 // Default Opus sample rate var preSkip uint16 = 0 var foundOpusHead bool // Scan through the file looking for Ogg pages for i := 0; i < len(data); { // Check if we have enough data to read Ogg page header if i+27 >= len(data) { break } // Verify Ogg page signature if string(data[i:i+4]) != "OggS" { // Skip until next potential page i++ continue } // Extract header fields granulePos := binary.LittleEndian.Uint64(data[i+6 : i+14]) pageSeqNum := binary.LittleEndian.Uint32(data[i+18 : i+22]) numSegments := int(data[i+26]) // Extract segment table if i+27+numSegments >= len(data) { break } segmentTable := data[i+27 : i+27+numSegments] // Calculate page size pageSize := 27 + numSegments for _, segLen := range segmentTable { pageSize += int(segLen) } // Check if we're looking at an OpusHead packet (should be in first few pages) if !foundOpusHead && pageSeqNum <= 1 { // Look for "OpusHead" marker in this page pageData := data[i : i+pageSize] headPos := bytes.Index(pageData, []byte("OpusHead")) if headPos >= 0 && headPos+12 < len(pageData) { // Found OpusHead, extract sample rate and pre-skip // OpusHead format: Magic(8) + Version(1) + Channels(1) + PreSkip(2) + SampleRate(4) + ... headPos += 8 // Skip "OpusHead" marker // PreSkip is 2 bytes at offset 10 if headPos+12 <= len(pageData) { preSkip = binary.LittleEndian.Uint16(pageData[headPos+10 : headPos+12]) sampleRate = binary.LittleEndian.Uint32(pageData[headPos+12 : headPos+16]) foundOpusHead = true fmt.Printf("Found OpusHead: sampleRate=%d, preSkip=%d\n", sampleRate, preSkip) } } } // Keep track of last valid granule position if granulePos != 0 { lastGranule = granulePos } // Move to next page i += pageSize } if !foundOpusHead { fmt.Println("Warning: OpusHead not found, using default values") } // Calculate duration based on granule position if lastGranule > 0 { // Formula for duration: (lastGranule - preSkip) / sampleRate durationSeconds := float64(lastGranule-uint64(preSkip)) / float64(sampleRate) duration = uint32(math.Ceil(durationSeconds)) fmt.Printf("Calculated Opus duration from granule: %f seconds (lastGranule=%d)\n", durationSeconds, lastGranule) } else { // Fallback to rough estimation if granule position not found fmt.Println("Warning: No valid granule position found, using estimation") durationEstimate := float64(len(data)) / 2000.0 // Very rough approximation duration = uint32(durationEstimate) } // Make sure we have a reasonable duration (at least 1 second, at most 300 seconds) if duration < 1 { duration = 1 } else if duration > 300 { duration = 300 } // Generate waveform waveform = placeholderWaveform(duration) fmt.Printf("Ogg Opus analysis: size=%d bytes, calculated duration=%d sec, waveform=%d bytes\n", len(data), duration, len(waveform)) return duration, waveform, nil } // min returns the smaller of x or y func min(x, y int) int { if x < y { return x } return y } // placeholderWaveform generates a synthetic waveform for WhatsApp voice messages // that appears natural with some variability based on the duration func placeholderWaveform(duration uint32) []byte { // WhatsApp expects a 64-byte waveform for voice messages const waveformLength = 64 waveform := make([]byte, waveformLength) // Seed the random number generator for consistent results with the same duration rand.Seed(int64(duration)) // Create a more natural looking waveform with some patterns and variability // rather than completely random values // Base amplitude and frequency - longer messages get faster frequency baseAmplitude := 35.0 frequencyFactor := float64(min(int(duration), 120)) / 30.0 for i := range waveform { // Position in the waveform (normalized 0-1) pos := float64(i) / float64(waveformLength) // Create a wave pattern with some randomness // Use multiple sine waves of different frequencies for more natural look val := baseAmplitude * math.Sin(pos*math.Pi*frequencyFactor*8) val += (baseAmplitude / 2) * math.Sin(pos*math.Pi*frequencyFactor*16) // Add some randomness to make it look more natural val += (rand.Float64() - 0.5) * 15 // Add some fade-in and fade-out effects fadeInOut := math.Sin(pos * math.Pi) val = val * (0.7 + 0.3*fadeInOut) // Center around 50 (typical voice baseline) val = val + 50 // Ensure values stay within WhatsApp's expected range (0-100) if val < 0 { val = 0 } else if val > 100 { val = 100 } waveform[i] = byte(val) } return waveform }