mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
2c317444c5
Downgrade outbound QQ audio to file upload when it exceeds the 60 second voice limit or its duration cannot be detected. Refs #1884
232 lines
4.7 KiB
Go
232 lines
4.7 KiB
Go
package qq
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const qqVoiceMaxDuration = 60 * time.Second
|
|
|
|
func qqAudioDuration(localPath, filename, contentType string) (time.Duration, bool, error) {
|
|
if localPath == "" {
|
|
return 0, false, nil
|
|
}
|
|
|
|
switch qqAudioDurationFormat(localPath, filename, contentType) {
|
|
case "wav":
|
|
return qqWAVDuration(localPath)
|
|
case "ogg":
|
|
return qqOggDuration(localPath)
|
|
default:
|
|
return 0, false, nil
|
|
}
|
|
}
|
|
|
|
func qqAudioDurationFormat(localPath, filename, contentType string) string {
|
|
contentType = strings.ToLower(contentType)
|
|
|
|
switch {
|
|
case strings.HasPrefix(contentType, "audio/wav"), strings.HasPrefix(contentType, "audio/x-wav"):
|
|
return "wav"
|
|
case strings.HasPrefix(contentType, "audio/ogg"),
|
|
contentType == "application/ogg",
|
|
contentType == "application/x-ogg":
|
|
return "ogg"
|
|
}
|
|
|
|
switch filepath.Ext(strings.ToLower(filename)) {
|
|
case ".wav":
|
|
return "wav"
|
|
case ".ogg", ".opus":
|
|
return "ogg"
|
|
}
|
|
|
|
switch filepath.Ext(strings.ToLower(localPath)) {
|
|
case ".wav":
|
|
return "wav"
|
|
case ".ogg", ".opus":
|
|
return "ogg"
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func qqWAVDuration(localPath string) (time.Duration, bool, error) {
|
|
file, err := os.Open(localPath)
|
|
if err != nil {
|
|
return 0, false, err
|
|
}
|
|
defer file.Close()
|
|
|
|
var header [12]byte
|
|
if _, err := io.ReadFull(file, header[:]); err != nil {
|
|
return 0, false, err
|
|
}
|
|
|
|
var order binary.ByteOrder
|
|
switch string(header[:4]) {
|
|
case "RIFF":
|
|
order = binary.LittleEndian
|
|
case "RIFX":
|
|
order = binary.BigEndian
|
|
default:
|
|
return 0, false, nil
|
|
}
|
|
|
|
if string(header[8:12]) != "WAVE" {
|
|
return 0, false, nil
|
|
}
|
|
|
|
var byteRate uint32
|
|
var dataSize uint32
|
|
var foundFmt bool
|
|
var foundData bool
|
|
|
|
for {
|
|
var chunkHeader [8]byte
|
|
if _, err := io.ReadFull(file, chunkHeader[:]); err != nil {
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
return 0, false, err
|
|
}
|
|
|
|
chunkSize := order.Uint32(chunkHeader[4:8])
|
|
switch string(chunkHeader[:4]) {
|
|
case "fmt ":
|
|
chunkData := make([]byte, chunkSize)
|
|
if _, err := io.ReadFull(file, chunkData); err != nil {
|
|
return 0, false, err
|
|
}
|
|
if len(chunkData) >= 12 {
|
|
byteRate = order.Uint32(chunkData[8:12])
|
|
foundFmt = true
|
|
}
|
|
case "data":
|
|
dataSize = chunkSize
|
|
foundData = true
|
|
if _, err := io.CopyN(io.Discard, file, int64(chunkSize)); err != nil {
|
|
return 0, false, err
|
|
}
|
|
default:
|
|
if _, err := io.CopyN(io.Discard, file, int64(chunkSize)); err != nil {
|
|
return 0, false, err
|
|
}
|
|
}
|
|
|
|
if chunkSize%2 == 1 {
|
|
if _, err := io.CopyN(io.Discard, file, 1); err != nil {
|
|
return 0, false, err
|
|
}
|
|
}
|
|
|
|
if foundFmt && foundData {
|
|
break
|
|
}
|
|
}
|
|
|
|
if !foundFmt || !foundData || byteRate == 0 {
|
|
return 0, false, nil
|
|
}
|
|
|
|
durationNS := int64(dataSize) * int64(time.Second) / int64(byteRate)
|
|
return time.Duration(durationNS), true, nil
|
|
}
|
|
|
|
func qqOggDuration(localPath string) (time.Duration, bool, error) {
|
|
file, err := os.Open(localPath)
|
|
if err != nil {
|
|
return 0, false, err
|
|
}
|
|
defer file.Close()
|
|
|
|
var firstPacket []byte
|
|
var codec string
|
|
var sampleRate uint32
|
|
var lastGranule uint64
|
|
var haveGranule bool
|
|
|
|
for {
|
|
var header [27]byte
|
|
if _, err := io.ReadFull(file, header[:]); err != nil {
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
return 0, false, err
|
|
}
|
|
|
|
if string(header[:4]) != "OggS" {
|
|
return 0, false, nil
|
|
}
|
|
|
|
pageSegments := int(header[26])
|
|
segments := make([]byte, pageSegments)
|
|
if _, err := io.ReadFull(file, segments); err != nil {
|
|
return 0, false, err
|
|
}
|
|
|
|
payloadLen := 0
|
|
for _, segLen := range segments {
|
|
payloadLen += int(segLen)
|
|
}
|
|
|
|
payload := make([]byte, payloadLen)
|
|
if _, err := io.ReadFull(file, payload); err != nil {
|
|
return 0, false, err
|
|
}
|
|
|
|
granule := binary.LittleEndian.Uint64(header[6:14])
|
|
if granule != ^uint64(0) {
|
|
lastGranule = granule
|
|
haveGranule = true
|
|
}
|
|
|
|
if codec == "" {
|
|
offset := 0
|
|
for _, segLen := range segments {
|
|
firstPacket = append(firstPacket, payload[offset:offset+int(segLen)]...)
|
|
offset += int(segLen)
|
|
if segLen < 255 {
|
|
codec, sampleRate = qqParseOggCodec(firstPacket)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if !haveGranule || codec == "" {
|
|
return 0, false, nil
|
|
}
|
|
|
|
switch codec {
|
|
case "opus":
|
|
return time.Duration(lastGranule) * time.Second / 48000, true, nil
|
|
case "vorbis":
|
|
if sampleRate == 0 {
|
|
return 0, false, nil
|
|
}
|
|
return time.Duration(lastGranule) * time.Second / time.Duration(sampleRate), true, nil
|
|
default:
|
|
return 0, false, nil
|
|
}
|
|
}
|
|
|
|
func qqParseOggCodec(packet []byte) (string, uint32) {
|
|
if len(packet) >= 8 && string(packet[:8]) == "OpusHead" {
|
|
return "opus", 48000
|
|
}
|
|
|
|
if len(packet) >= 16 && packet[0] == 0x01 && string(packet[1:7]) == "vorbis" {
|
|
sampleRate := binary.LittleEndian.Uint32(packet[12:16])
|
|
if sampleRate > 0 {
|
|
return "vorbis", sampleRate
|
|
}
|
|
}
|
|
|
|
return "", 0
|
|
}
|