143 lines
5.3 KiB
Go
143 lines
5.3 KiB
Go
// Package audio реализует минимальный набор для аудио-стеганографии в
|
||
// CTF: разбор WAV, быстрое преобразование Фурье и построение
|
||
// спектрограммы. Только WAV, не MP3/FLAC — и это осознанный выбор, не
|
||
// недоделка: сжатие с потерями убивает как раз ту мелкую структуру
|
||
// спектра, в которую прячут сообщения, так что задачи на эту тему
|
||
// в CTF практически всегда именно в несжатом PCM WAV.
|
||
package audio
|
||
|
||
import (
|
||
"encoding/binary"
|
||
"fmt"
|
||
"os"
|
||
)
|
||
|
||
// WAV — результат разбора файла: моно-сигнал, нормализованный в
|
||
// диапазон [-1, 1] независимо от исходной битности. Многоканальные
|
||
// файлы усредняются в моно при разборе — для спектрограммы порядок
|
||
// каналов не важен, а городить в UI выбор канала ради этой конкретной
|
||
// задачи не стали.
|
||
type WAV struct {
|
||
SampleRate int
|
||
Samples []float64
|
||
}
|
||
|
||
// LoadWAV разбирает несжатый PCM WAV (RIFF/WAVE, formatTag=1).
|
||
func LoadWAV(path string) (*WAV, error) {
|
||
data, err := os.ReadFile(path)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("чтение %s: %w", path, err)
|
||
}
|
||
if len(data) < 44 || string(data[0:4]) != "RIFF" || string(data[8:12]) != "WAVE" {
|
||
return nil, fmt.Errorf("не похоже на WAV-файл (нет заголовка RIFF/WAVE)")
|
||
}
|
||
|
||
var (
|
||
sampleRate int
|
||
bitsPerSample int
|
||
channels int
|
||
audioFormat uint16
|
||
pcmData []byte
|
||
foundFmt bool
|
||
foundData bool
|
||
)
|
||
|
||
pos := 12
|
||
for pos+8 <= len(data) {
|
||
chunkID := string(data[pos : pos+4])
|
||
chunkSize := int(binary.LittleEndian.Uint32(data[pos+4 : pos+8]))
|
||
chunkStart := pos + 8
|
||
if chunkStart+chunkSize > len(data) || chunkSize < 0 {
|
||
chunkSize = len(data) - chunkStart // повреждённый/обрезанный файл — берём что есть, не падаем
|
||
}
|
||
if chunkSize < 0 {
|
||
break
|
||
}
|
||
|
||
switch chunkID {
|
||
case "fmt ":
|
||
if chunkSize < 16 {
|
||
return nil, fmt.Errorf("повреждённый чанк fmt")
|
||
}
|
||
audioFormat = binary.LittleEndian.Uint16(data[chunkStart : chunkStart+2])
|
||
channels = int(binary.LittleEndian.Uint16(data[chunkStart+2 : chunkStart+4]))
|
||
sampleRate = int(binary.LittleEndian.Uint32(data[chunkStart+4 : chunkStart+8]))
|
||
bitsPerSample = int(binary.LittleEndian.Uint16(data[chunkStart+14 : chunkStart+16]))
|
||
foundFmt = true
|
||
case "data":
|
||
pcmData = data[chunkStart : chunkStart+chunkSize]
|
||
foundData = true
|
||
}
|
||
|
||
pos = chunkStart + chunkSize
|
||
if pos%2 == 1 { // чанки RIFF выровнены по чётной границе
|
||
pos++
|
||
}
|
||
}
|
||
|
||
if !foundFmt || !foundData {
|
||
return nil, fmt.Errorf("не найден чанк fmt или data — файл повреждён или это не WAV")
|
||
}
|
||
if audioFormat != 1 {
|
||
return nil, fmt.Errorf("поддерживается только несжатый PCM (formatTag=1), получен %d — если файл сжат (MP3/ADPCM и т.п.), сначала перекодируй в PCM WAV", audioFormat)
|
||
}
|
||
if channels < 1 {
|
||
return nil, fmt.Errorf("некорректное число каналов: %d", channels)
|
||
}
|
||
|
||
samples, err := decodePCM(pcmData, bitsPerSample, channels)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if len(samples) == 0 {
|
||
return nil, fmt.Errorf("в файле нет сэмплов")
|
||
}
|
||
return &WAV{SampleRate: sampleRate, Samples: samples}, nil
|
||
}
|
||
|
||
// decodePCM разбирает сырые PCM-байты в моно float64 [-1, 1], усредняя
|
||
// каналы, если их больше одного.
|
||
func decodePCM(data []byte, bitsPerSample, channels int) ([]float64, error) {
|
||
bytesPerSample := bitsPerSample / 8
|
||
if bytesPerSample == 0 {
|
||
return nil, fmt.Errorf("неподдерживаемая битность: %d", bitsPerSample)
|
||
}
|
||
frameSize := bytesPerSample * channels
|
||
if frameSize == 0 {
|
||
return nil, fmt.Errorf("некорректный размер фрейма")
|
||
}
|
||
numFrames := len(data) / frameSize
|
||
|
||
samples := make([]float64, numFrames)
|
||
for i := 0; i < numFrames; i++ {
|
||
var sum float64
|
||
for c := 0; c < channels; c++ {
|
||
off := i*frameSize + c*bytesPerSample
|
||
sum += decodeSample(data[off:off+bytesPerSample], bitsPerSample)
|
||
}
|
||
samples[i] = sum / float64(channels)
|
||
}
|
||
return samples, nil
|
||
}
|
||
|
||
func decodeSample(b []byte, bitsPerSample int) float64 {
|
||
switch bitsPerSample {
|
||
case 8:
|
||
// 8-битный PCM в WAV — беззнаковый (0..255, тишина = 128).
|
||
return (float64(b[0]) - 128) / 128
|
||
case 16:
|
||
v := int16(binary.LittleEndian.Uint16(b))
|
||
return float64(v) / 32768
|
||
case 24:
|
||
v := int32(b[0]) | int32(b[1])<<8 | int32(b[2])<<16
|
||
if v&0x800000 != 0 {
|
||
v -= 0x1000000 // старший бит 24-битного числа установлен — это отрицательное значение, поправляем через вычитание 2^24
|
||
}
|
||
return float64(v) / 8388608
|
||
case 32:
|
||
v := int32(binary.LittleEndian.Uint32(b))
|
||
return float64(v) / 2147483648
|
||
default:
|
||
return 0
|
||
}
|
||
}
|