feat(audio): 音频管理器——3D 衰减、声源池淘汰、分类音量、程序化合成音(beep+oto 纯 Go)
This commit is contained in:
26
go.mod
26
go.mod
@@ -2,14 +2,24 @@ module mc
|
||||
|
||||
go 1.26.4
|
||||
|
||||
require gopkg.in/yaml.v3 v3.0.1
|
||||
require (
|
||||
github.com/ebitengine/oto/v3 v3.4.0
|
||||
github.com/faiface/beep v1.1.0
|
||||
github.com/fsnotify/fsnotify v1.10.1
|
||||
github.com/go-gl/gl v0.0.0-20260331235117-4566fea9a276
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20260802143932-8fa725040a18
|
||||
github.com/klauspost/compress v1.19.2
|
||||
github.com/ojrac/opensimplex-go v1.0.2
|
||||
github.com/yuin/gopher-lua v1.1.2
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/fsnotify/fsnotify v1.10.1 // indirect
|
||||
github.com/go-gl/gl v0.0.0-20260331235117-4566fea9a276 // indirect
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20260802143932-8fa725040a18 // indirect
|
||||
github.com/klauspost/compress v1.19.2 // indirect
|
||||
github.com/ojrac/opensimplex-go v1.0.2 // indirect
|
||||
github.com/yuin/gopher-lua v1.1.2 // indirect
|
||||
golang.org/x/sys v0.13.0 // indirect
|
||||
github.com/ebitengine/purego v0.9.0 // indirect
|
||||
github.com/hajimehoshi/oto v0.7.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 // indirect
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067 // indirect
|
||||
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6 // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
)
|
||||
|
||||
44
go.sum
44
go.sum
@@ -1,17 +1,57 @@
|
||||
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||
github.com/d4l3k/messagediff v1.2.2-0.20190829033028-7e0a312ae40b/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo=
|
||||
github.com/ebitengine/oto/v3 v3.4.0 h1:br0PgASsEWaoWn38b2Goe7m1GKFYfNgnsjSd5Gg+/bQ=
|
||||
github.com/ebitengine/oto/v3 v3.4.0/go.mod h1:IOleLVD0m+CMak3mRVwsYY8vTctQgOM0iiL6S7Ar7eI=
|
||||
github.com/ebitengine/purego v0.9.0 h1:mh0zpKBIXDceC63hpvPuGLiJ8ZAa3DfrFTudmfi8A4k=
|
||||
github.com/ebitengine/purego v0.9.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/faiface/beep v1.1.0 h1:A2gWP6xf5Rh7RG/p9/VAW2jRSDEGQm5sbOb38sf5d4c=
|
||||
github.com/faiface/beep v1.1.0/go.mod h1:6I8p6kK2q4opL/eWb+kAkk38ehnTunWeToJB+s51sT4=
|
||||
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
|
||||
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
|
||||
github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs=
|
||||
github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498=
|
||||
github.com/go-audio/wav v1.0.0/go.mod h1:3yoReyQOsiARkvPl3ERCi8JFjihzG6WhjYpZCf5zAWE=
|
||||
github.com/go-gl/gl v0.0.0-20260331235117-4566fea9a276 h1:IO5P06Pcj9K04d+l4nrf3c2U56+dAotIFG6u4P1wAHI=
|
||||
github.com/go-gl/gl v0.0.0-20260331235117-4566fea9a276/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20260802143932-8fa725040a18 h1:c/72gzBO3eZPUVuHPnfyvt79XoIEi7KYGBY/8J/BpeI=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20260802143932-8fa725040a18/go.mod h1:SyRD8YfuKk+ZXlDqYiqe1qMSqjNgtHzBTG810KUagMc=
|
||||
github.com/hajimehoshi/go-mp3 v0.3.0/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM=
|
||||
github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI=
|
||||
github.com/hajimehoshi/oto v0.7.1 h1:I7maFPz5MBCwiutOrz++DLdbr4rTzBsbBuV2VpgU9kk=
|
||||
github.com/hajimehoshi/oto v0.7.1/go.mod h1:wovJ8WWMfFKvP587mhHgot/MBr4DnNy9m6EepeVGnos=
|
||||
github.com/icza/bitio v1.0.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A=
|
||||
github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6/go.mod h1:xQig96I1VNBDIWGCdTt54nHt6EeI639SmHycLYL7FkA=
|
||||
github.com/jfreymuth/oggvorbis v1.0.1/go.mod h1:NqS+K+UXKje0FUYUPosyQ+XTVvjmVjps1aEZH1sumIk=
|
||||
github.com/jfreymuth/vorbis v1.0.0/go.mod h1:8zy3lUAm9K/rJJk223RKy6vjCZTWC61NA2QD06bfOE0=
|
||||
github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi26R8=
|
||||
github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
|
||||
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mewkiz/flac v1.0.7/go.mod h1:yU74UH277dBUpqxPouHSQIar3G1X/QIclVbFahSd1pU=
|
||||
github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2/go.mod h1:3E2FUC/qYUfM8+r9zAwpeHJzqRVVMIYnpzD/clwWxyA=
|
||||
github.com/ojrac/opensimplex-go v1.0.2 h1:l4vs0D+JCakcu5OV0kJ99oEaWJfggSc9jiLpxaWvSzs=
|
||||
github.com/ojrac/opensimplex-go v1.0.2/go.mod h1:NwbXFFbXcdGgIFdiA7/REME+7n/lOf1TuEbLiZYOWnM=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/yuin/gopher-lua v1.1.2 h1:yF/FjE3hD65tBbt0VXLE13HWS9h34fdzJmrWRXwobGA=
|
||||
github.com/yuin/gopher-lua v1.1.2/go.mod h1:7aRmXIWl37SqRf0koeyylBEzJ+aPt8A+mmkQ4f1ntR8=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 h1:idBdZTd9UioThJp8KpM/rTSinK/ChZFBE43/WtIy8zg=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067 h1:KYGJGHOQy8oSi1fDlSpcZF0+juKwk/hEMv5SiwHogR0=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6 h1:vyLBGJPIl9ZYbcQFM2USFmJBK6KI+t+z6jL0lbwjrnc=
|
||||
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
152
internal/audio/audio.go
Normal file
152
internal/audio/audio.go
Normal file
@@ -0,0 +1,152 @@
|
||||
// Package audio 音频管理器:3D 衰减、声源上限、分类音量(场景/音频.md §2–§4)。
|
||||
//
|
||||
// 无音频文件阶段用程序化合成音(正弦/噪声包络);播放后端为 beep + oto(Windows 纯 Go,
|
||||
// 基于 purego,无需 CGO)。
|
||||
package audio
|
||||
|
||||
import (
|
||||
"math"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ebitengine/oto/v3"
|
||||
"github.com/faiface/beep"
|
||||
"github.com/faiface/beep/effects"
|
||||
"github.com/faiface/beep/speaker"
|
||||
)
|
||||
|
||||
// 声源上限(音频.md §3:同时播放 ≤64,超出按最远淘汰)。
|
||||
const maxSources = 64
|
||||
|
||||
// 音量分类(音频.md §4)。
|
||||
type category uint8
|
||||
|
||||
const (
|
||||
catSFX category = iota // 音效
|
||||
catMusic // 音乐
|
||||
catAmbient // 环境
|
||||
)
|
||||
|
||||
// Manager 音频管理器(单例)。
|
||||
type Manager struct {
|
||||
mu sync.Mutex
|
||||
enabled bool
|
||||
ctx *oto.Context
|
||||
master float64
|
||||
vols [3]float64 // sfx / music / ambient
|
||||
sources []*source
|
||||
listener [3]float64 // 玩家位置
|
||||
}
|
||||
|
||||
// source 一个正在播放的声源。
|
||||
type source struct {
|
||||
key string
|
||||
pos [3]float64 // 世界坐标
|
||||
dist float64 // 距玩家距离
|
||||
ctrl *beep.Ctrl
|
||||
cat category
|
||||
start time.Time
|
||||
}
|
||||
|
||||
// New 创建音频管理器(找不到音频设备时降级为静默——服务器/CI 安全)。
|
||||
func New() *Manager {
|
||||
m := &Manager{master: 1.0, vols: [3]float64{1, 1, 1}}
|
||||
const (
|
||||
sampleRate = 44100
|
||||
bufferSize = 100 * time.Millisecond
|
||||
)
|
||||
ctx, _, err := oto.NewContext(&oto.NewContextOptions{
|
||||
SampleRate: sampleRate,
|
||||
ChannelCount: 2,
|
||||
BufferSize: bufferSize,
|
||||
})
|
||||
if err != nil {
|
||||
return m // 无设备:静默模式
|
||||
}
|
||||
m.ctx = ctx
|
||||
m.enabled = true
|
||||
speaker.Init(sampleRate, int(sampleRate*0.1))
|
||||
return m
|
||||
}
|
||||
|
||||
// SetListener 更新玩家位置(每帧/tick)。
|
||||
func (m *Manager) SetListener(x, y, z float64) {
|
||||
m.mu.Lock()
|
||||
m.listener = [3]float64{x, y, z}
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
// SetVolumes 设置分类音量(master × 分类)。
|
||||
func (m *Manager) SetVolumes(master, sfx, music, ambient float64) {
|
||||
m.mu.Lock()
|
||||
m.master, m.vols[catSFX], m.vols[catMusic], m.vols[catAmbient] = master, sfx, music, ambient
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
// Play 播放一个音效(3D 衰减,音频.md §3):key 为音效键,pos 为声源世界坐标。
|
||||
func (m *Manager) Play(key string, x, y, z float64) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if !m.enabled {
|
||||
return
|
||||
}
|
||||
dist := math.Sqrt((x-m.listener[0])*(x-m.listener[0]) + (y-m.listener[1])*(y-m.listener[1]) + (z-m.listener[2])*(z-m.listener[2]))
|
||||
gain := attenuation(dist)
|
||||
if gain <= 0.01 {
|
||||
return // 超出衰减范围:不发声
|
||||
}
|
||||
// 声源池:满员时淘汰最远(音频.md §3)
|
||||
if len(m.sources) >= maxSources {
|
||||
worst := -1
|
||||
worstDist := -1.0
|
||||
for i, s := range m.sources {
|
||||
if s.dist > worstDist {
|
||||
worstDist = s.dist
|
||||
worst = i
|
||||
}
|
||||
}
|
||||
if worst >= 0 {
|
||||
m.sources[worst].ctrl.Streamer = nil
|
||||
m.sources[worst].ctrl.Paused = true
|
||||
m.sources = append(m.sources[:worst], m.sources[worst+1:]...)
|
||||
}
|
||||
}
|
||||
// 程序化合成:按音效键选择波形(无音频文件的占位方案,音频.md §6)
|
||||
streamer := synthFor(key)
|
||||
ctrl := &beep.Ctrl{Streamer: streamer, Paused: false}
|
||||
vol := &effects.Volume{Streamer: ctrl, Base: 2, Volume: math.Log2(0.1+gain)*m.master*m.vols[catSFX] + 1}
|
||||
speaker.Play(vol)
|
||||
m.sources = append(m.sources, &source{key: key, pos: [3]float64{x, y, z}, dist: dist, ctrl: ctrl, start: time.Now()})
|
||||
}
|
||||
|
||||
// attenuation 距离衰减:线性,maxDist 20(音频.md §3)。
|
||||
func attenuation(dist float64) float64 {
|
||||
const maxDist = 20.0
|
||||
if dist >= maxDist {
|
||||
return 0
|
||||
}
|
||||
return 1 - dist/maxDist
|
||||
}
|
||||
|
||||
// Close 释放设备。
|
||||
func (m *Manager) Close() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.ctx != nil {
|
||||
_ = m.ctx.Suspend()
|
||||
m.ctx = nil
|
||||
}
|
||||
m.enabled = false
|
||||
}
|
||||
|
||||
// synthFor 按音效键生成占位波形(后续替换为真实音效文件)。
|
||||
func synthFor(key string) beep.Streamer {
|
||||
switch key {
|
||||
case "block.stone.break", "block.grass.break":
|
||||
return noiseBurst(0.12)
|
||||
case "block.place", "step":
|
||||
return sineBurst(220, 0.06)
|
||||
default:
|
||||
return sineBurst(440, 0.1)
|
||||
}
|
||||
}
|
||||
50
internal/audio/audio_test.go
Normal file
50
internal/audio/audio_test.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package audio
|
||||
|
||||
import "testing"
|
||||
|
||||
// TestAttenuation 距离衰减公式(音频.md §3)。
|
||||
func TestAttenuation(t *testing.T) {
|
||||
if a := attenuation(0); a != 1 {
|
||||
t.Fatalf("零距离衰减期望 1,实际 %v", a)
|
||||
}
|
||||
if a := attenuation(10); a != 0.5 {
|
||||
t.Fatalf("10 格衰减期望 0.5,实际 %v", a)
|
||||
}
|
||||
if a := attenuation(20); a != 0 {
|
||||
t.Fatalf("最大距离衰减期望 0,实际 %v", a)
|
||||
}
|
||||
if a := attenuation(30); a != 0 {
|
||||
t.Fatalf("超距衰减期望 0,实际 %v", a)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBurstStreamer 合成流:有限长度与数据填充。
|
||||
func TestBurstStreamer(t *testing.T) {
|
||||
b := sineBurst(440, 0.05)
|
||||
samples := make([][2]float64, 1024)
|
||||
total := 0
|
||||
for {
|
||||
n, ok := b.Stream(samples)
|
||||
total += n
|
||||
if ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
if total != b.Len() {
|
||||
t.Fatalf("流长度期望 %d,实际 %d", b.Len(), total)
|
||||
}
|
||||
if total < 1000 {
|
||||
t.Fatalf("采样数异常: %d", total)
|
||||
}
|
||||
}
|
||||
|
||||
// TestManagerSilent 无音频设备环境(CI/服务器)下降级为静默、不崩溃。
|
||||
func TestManagerSilent(t *testing.T) {
|
||||
m := New() // 可能无设备:必须不 panic
|
||||
defer m.Close()
|
||||
m.SetVolumes(1, 1, 1, 1)
|
||||
m.SetListener(0, 0, 0)
|
||||
for i := 0; i < 100; i++ {
|
||||
m.Play("block.stone.break", float64(i), 0, 0) // 池满淘汰也不崩
|
||||
}
|
||||
}
|
||||
69
internal/audio/synth.go
Normal file
69
internal/audio/synth.go
Normal file
@@ -0,0 +1,69 @@
|
||||
// 程序化音效合成:正弦脉冲与噪声脉冲(无音频文件的占位实现,音频.md §6)。
|
||||
package audio
|
||||
|
||||
import (
|
||||
"math"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/faiface/beep"
|
||||
)
|
||||
|
||||
// sineBurst 正弦脉冲(频率 Hz、时长秒)。
|
||||
func sineBurst(freq float64, dur float64) *burst {
|
||||
n := int(44100 * dur)
|
||||
return &burst{n: n, gen: func(t int) float64 {
|
||||
env := 1 - float64(t)/float64(n) // 线性衰减包络
|
||||
return env * 0.5 * math.Sin(2*math.Pi*freq*float64(t)/44100)
|
||||
}}
|
||||
}
|
||||
|
||||
// noiseBurst 噪声脉冲(破坏音效感)。
|
||||
func noiseBurst(dur float64) *burst {
|
||||
n := int(44100 * dur)
|
||||
rng := rand.New(rand.NewSource(42))
|
||||
return &burst{n: n, gen: func(t int) float64 {
|
||||
env := 1 - float64(t)/float64(n)
|
||||
return env * 0.4 * (rng.Float64()*2 - 1)
|
||||
}}
|
||||
}
|
||||
|
||||
// burst 有限长度合成流。
|
||||
type burst struct {
|
||||
n int
|
||||
pos int
|
||||
gen func(t int) float64
|
||||
}
|
||||
|
||||
// Stream beep.Streamer:填充采样。
|
||||
func (b *burst) Stream(samples [][2]float64) (int, bool) {
|
||||
i := 0
|
||||
for ; i < len(samples) && b.pos < b.n; i++ {
|
||||
v := b.gen(b.pos)
|
||||
samples[i][0] = v
|
||||
samples[i][1] = v
|
||||
b.pos++
|
||||
}
|
||||
return i, b.pos >= b.n
|
||||
}
|
||||
|
||||
// Err 无错误。
|
||||
func (b *burst) Err() error { return nil }
|
||||
|
||||
// Len 时长采样数。
|
||||
func (b *burst) Len() int { return b.n }
|
||||
|
||||
// Position 当前位置。
|
||||
func (b *burst) Position() int { return b.pos }
|
||||
|
||||
// Seek 跳转。
|
||||
func (b *burst) Seek(p int) error {
|
||||
if p < 0 || p > b.n {
|
||||
p = 0
|
||||
}
|
||||
b.pos = p
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ beep.StreamSeeker = (*burst)(nil)
|
||||
var _ = time.Second
|
||||
BIN
tools/zig.zip
BIN
tools/zig.zip
Binary file not shown.
Reference in New Issue
Block a user