mirror of
https://github.com/hajimehoshi/ebiten.git
synced 2025-01-12 20:18:59 +01:00
audio: Consume necessary and sufficient data on each Update (#205) (WIP)
This commit is contained in:
parent
4e2d661541
commit
715b5df7e1
@ -36,23 +36,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type mixingStream struct {
|
type mixingStream struct {
|
||||||
sampleRate int
|
sampleRate int
|
||||||
writtenBytes int
|
players map[*Player]struct{}
|
||||||
frames int
|
|
||||||
players map[*Player]struct{}
|
|
||||||
|
|
||||||
// Note that Read (and other methods) need to be concurrent safe
|
// Note that Read (and other methods) need to be concurrent safe
|
||||||
// because Read is called from another groutine (see NewContext).
|
// because Read is called from another groutine (see NewContext).
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func min(a, b int) int {
|
|
||||||
if a < b {
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
channelNum = 2
|
channelNum = 2
|
||||||
bytesPerSample = 2
|
bytesPerSample = 2
|
||||||
@ -62,26 +53,24 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (s *mixingStream) SampleRate() int {
|
func (s *mixingStream) SampleRate() int {
|
||||||
s.RLock()
|
|
||||||
defer s.RUnlock()
|
|
||||||
return s.sampleRate
|
return s.sampleRate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func min(a, b int) int {
|
||||||
|
if a < b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
func (s *mixingStream) Read(b []byte) (int, error) {
|
func (s *mixingStream) Read(b []byte) (int, error) {
|
||||||
s.Lock()
|
s.Lock()
|
||||||
defer s.Unlock()
|
defer s.Unlock()
|
||||||
|
|
||||||
bytesPerFrame := s.sampleRate * bytesPerSample * channelNum / ebiten.FPS
|
|
||||||
x := s.frames*bytesPerFrame + len(b)
|
|
||||||
if x <= s.writtenBytes {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(s.players) == 0 {
|
if len(s.players) == 0 {
|
||||||
l := min(len(b), x-s.writtenBytes)
|
l := len(b)
|
||||||
l &= mask
|
l &= mask
|
||||||
copy(b, make([]byte, l))
|
copy(b, make([]byte, l))
|
||||||
s.writtenBytes += l
|
|
||||||
return l, nil
|
return l, nil
|
||||||
}
|
}
|
||||||
closed := []*Player{}
|
closed := []*Player{}
|
||||||
@ -120,17 +109,9 @@ func (s *mixingStream) Read(b []byte) (int, error) {
|
|||||||
for _, p := range closed {
|
for _, p := range closed {
|
||||||
delete(s.players, p)
|
delete(s.players, p)
|
||||||
}
|
}
|
||||||
s.writtenBytes += l
|
|
||||||
return l, nil
|
return l, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *mixingStream) update() error {
|
|
||||||
s.Lock()
|
|
||||||
defer s.Unlock()
|
|
||||||
s.frames++
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *mixingStream) newPlayer(src ReadSeekCloser) (*Player, error) {
|
func (s *mixingStream) newPlayer(src ReadSeekCloser) (*Player, error) {
|
||||||
s.Lock()
|
s.Lock()
|
||||||
defer s.Unlock()
|
defer s.Unlock()
|
||||||
@ -219,40 +200,26 @@ func (s *mixingStream) playerCurrent(player *Player) time.Duration {
|
|||||||
// You can also call Update independently from the game loop as 'async mode'.
|
// You can also call Update independently from the game loop as 'async mode'.
|
||||||
// In this case, audio goes on even when the game stops e.g. by diactivating the screen.
|
// In this case, audio goes on even when the game stops e.g. by diactivating the screen.
|
||||||
type Context struct {
|
type Context struct {
|
||||||
stream *mixingStream
|
stream *mixingStream
|
||||||
errorCh chan error
|
driver *driver.Player
|
||||||
|
frames int
|
||||||
|
writtenBytes int
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewContext creates a new audio context with the given sample rate (e.g. 44100).
|
// NewContext creates a new audio context with the given sample rate (e.g. 44100).
|
||||||
func NewContext(sampleRate int) (*Context, error) {
|
func NewContext(sampleRate int) (*Context, error) {
|
||||||
// TODO: Panic if one context exists.
|
// TODO: Panic if one context exists.
|
||||||
c := &Context{
|
c := &Context{}
|
||||||
errorCh: make(chan error),
|
|
||||||
}
|
|
||||||
c.stream = &mixingStream{
|
c.stream = &mixingStream{
|
||||||
sampleRate: sampleRate,
|
sampleRate: sampleRate,
|
||||||
players: map[*Player]struct{}{},
|
players: map[*Player]struct{}{},
|
||||||
}
|
}
|
||||||
// TODO: Rename this other than player
|
// TODO: Rename this other than player
|
||||||
p, err := driver.NewPlayer(c.stream, sampleRate, channelNum, bytesPerSample)
|
p, err := driver.NewPlayer(sampleRate, channelNum, bytesPerSample)
|
||||||
|
c.driver = p
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
go func() {
|
|
||||||
// TODO: Is it OK to close asap?
|
|
||||||
defer p.Close()
|
|
||||||
for {
|
|
||||||
err := p.Proceed()
|
|
||||||
if err == io.EOF {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
c.errorCh <- err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
time.Sleep(1 * time.Millisecond)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -264,12 +231,27 @@ func NewContext(sampleRate int) (*Context, error) {
|
|||||||
// you will find audio stops when the game stops e.g. when the window is deactivated.
|
// you will find audio stops when the game stops e.g. when the window is deactivated.
|
||||||
// In async mode, the audio never stops even when the game stops.
|
// In async mode, the audio never stops even when the game stops.
|
||||||
func (c *Context) Update() error {
|
func (c *Context) Update() error {
|
||||||
select {
|
c.frames++
|
||||||
case err := <-c.errorCh:
|
bytesPerFrame := c.stream.sampleRate * bytesPerSample * channelNum / ebiten.FPS
|
||||||
|
l := (c.frames * bytesPerFrame) - c.writtenBytes
|
||||||
|
l &= mask
|
||||||
|
c.writtenBytes += l
|
||||||
|
buf := make([]byte, l)
|
||||||
|
n, err := io.ReadFull(c.stream, buf)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
default:
|
|
||||||
}
|
}
|
||||||
return c.stream.update()
|
if n != len(buf) {
|
||||||
|
return c.driver.Close()
|
||||||
|
}
|
||||||
|
err = c.driver.Proceed(buf)
|
||||||
|
if err == io.EOF {
|
||||||
|
return c.driver.Close()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SampleRate returns the sample rate.
|
// SampleRate returns the sample rate.
|
||||||
|
@ -18,13 +18,11 @@ package driver
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/gopherjs/gopherjs/js"
|
"github.com/gopherjs/gopherjs/js"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Player struct {
|
type Player struct {
|
||||||
src io.Reader
|
|
||||||
sampleRate int
|
sampleRate int
|
||||||
channelNum int
|
channelNum int
|
||||||
bytesPerSample int
|
bytesPerSample int
|
||||||
@ -33,7 +31,7 @@ type Player struct {
|
|||||||
bufferSource *js.Object
|
bufferSource *js.Object
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPlayer(src io.Reader, sampleRate, channelNum, bytesPerSample int) (*Player, error) {
|
func NewPlayer(sampleRate, channelNum, bytesPerSample int) (*Player, error) {
|
||||||
class := js.Global.Get("AudioContext")
|
class := js.Global.Get("AudioContext")
|
||||||
if class == js.Undefined {
|
if class == js.Undefined {
|
||||||
class = js.Global.Get("webkitAudioContext")
|
class = js.Global.Get("webkitAudioContext")
|
||||||
@ -42,7 +40,6 @@ func NewPlayer(src io.Reader, sampleRate, channelNum, bytesPerSample int) (*Play
|
|||||||
return nil, errors.New("driver: audio couldn't be initialized")
|
return nil, errors.New("driver: audio couldn't be initialized")
|
||||||
}
|
}
|
||||||
p := &Player{
|
p := &Player{
|
||||||
src: src,
|
|
||||||
sampleRate: sampleRate,
|
sampleRate: sampleRate,
|
||||||
channelNum: channelNum,
|
channelNum: channelNum,
|
||||||
bytesPerSample: bytesPerSample,
|
bytesPerSample: bytesPerSample,
|
||||||
@ -63,37 +60,29 @@ func toLR(data []byte) ([]int16, []int16) {
|
|||||||
return l, r
|
return l, r
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
func (p *Player) Proceed(data []byte) error {
|
||||||
// 1024 seems not enough (some noise remains after the tab is deactivated).
|
|
||||||
bufferSize = 2048
|
|
||||||
)
|
|
||||||
|
|
||||||
func (p *Player) Proceed() error {
|
|
||||||
c := int64(p.context.Get("currentTime").Float() * float64(p.sampleRate))
|
c := int64(p.context.Get("currentTime").Float() * float64(p.sampleRate))
|
||||||
if p.positionInSamples < c {
|
if p.positionInSamples < c {
|
||||||
p.positionInSamples = c
|
p.positionInSamples = c
|
||||||
}
|
}
|
||||||
b := make([]byte, bufferSize)
|
n := len(data)
|
||||||
n, err := p.src.Read(b)
|
buf := p.context.Call("createBuffer", p.channelNum, n/p.bytesPerSample/p.channelNum, p.sampleRate)
|
||||||
if 0 < n {
|
l := buf.Call("getChannelData", 0)
|
||||||
buf := p.context.Call("createBuffer", p.channelNum, n/p.bytesPerSample/p.channelNum, p.sampleRate)
|
r := buf.Call("getChannelData", 1)
|
||||||
l := buf.Call("getChannelData", 0)
|
il, ir := toLR(data)
|
||||||
r := buf.Call("getChannelData", 1)
|
const max = 1 << 15
|
||||||
il, ir := toLR(b[:n])
|
for i := 0; i < len(il); i++ {
|
||||||
const max = 1 << 15
|
l.SetIndex(i, float64(il[i])/max)
|
||||||
for i := 0; i < len(il); i++ {
|
r.SetIndex(i, float64(ir[i])/max)
|
||||||
l.SetIndex(i, float64(il[i])/max)
|
|
||||||
r.SetIndex(i, float64(ir[i])/max)
|
|
||||||
}
|
|
||||||
p.bufferSource = p.context.Call("createBufferSource")
|
|
||||||
p.bufferSource.Set("buffer", buf)
|
|
||||||
p.bufferSource.Call("connect", p.context.Get("destination"))
|
|
||||||
p.bufferSource.Call("start", float64(p.positionInSamples)/float64(p.sampleRate))
|
|
||||||
// Call 'stop' or we'll get noisy sound especially on Chrome.
|
|
||||||
p.bufferSource.Call("stop", float64(p.positionInSamples+int64(len(il)))/float64(p.sampleRate))
|
|
||||||
p.positionInSamples += int64(len(il))
|
|
||||||
}
|
}
|
||||||
return err
|
p.bufferSource = p.context.Call("createBufferSource")
|
||||||
|
p.bufferSource.Set("buffer", buf)
|
||||||
|
p.bufferSource.Call("connect", p.context.Get("destination"))
|
||||||
|
p.bufferSource.Call("start", float64(p.positionInSamples)/float64(p.sampleRate))
|
||||||
|
// Call 'stop' or we'll get noisy sound especially on Chrome.
|
||||||
|
p.bufferSource.Call("stop", float64(p.positionInSamples+int64(len(il)))/float64(p.sampleRate))
|
||||||
|
p.positionInSamples += int64(len(il))
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Player) Close() error {
|
func (p *Player) Close() error {
|
||||||
|
@ -17,8 +17,8 @@
|
|||||||
package driver
|
package driver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
"github.com/hajimehoshi/go-openal/openal"
|
"github.com/hajimehoshi/go-openal/openal"
|
||||||
@ -35,7 +35,6 @@ type Player struct {
|
|||||||
alDevice *openal.Device
|
alDevice *openal.Device
|
||||||
alSource openal.Source
|
alSource openal.Source
|
||||||
alBuffers []openal.Buffer
|
alBuffers []openal.Buffer
|
||||||
source io.Reader
|
|
||||||
sampleRate int
|
sampleRate int
|
||||||
isClosed bool
|
isClosed bool
|
||||||
alFormat openal.Format
|
alFormat openal.Format
|
||||||
@ -55,7 +54,7 @@ func alFormat(channelNum, bytesPerSample int) openal.Format {
|
|||||||
panic(fmt.Sprintf("driver: invalid channel num (%d) or bytes per sample (%d)", channelNum, bytesPerSample))
|
panic(fmt.Sprintf("driver: invalid channel num (%d) or bytes per sample (%d)", channelNum, bytesPerSample))
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPlayer(src io.Reader, sampleRate, channelNum, bytesPerSample int) (*Player, error) {
|
func NewPlayer(sampleRate, channelNum, bytesPerSample int) (*Player, error) {
|
||||||
d := openal.OpenDevice("")
|
d := openal.OpenDevice("")
|
||||||
if d == nil {
|
if d == nil {
|
||||||
return nil, fmt.Errorf("driver: OpenDevice must not return nil")
|
return nil, fmt.Errorf("driver: OpenDevice must not return nil")
|
||||||
@ -78,39 +77,31 @@ func NewPlayer(src io.Reader, sampleRate, channelNum, bytesPerSample int) (*Play
|
|||||||
alDevice: d,
|
alDevice: d,
|
||||||
alSource: s,
|
alSource: s,
|
||||||
alBuffers: []openal.Buffer{},
|
alBuffers: []openal.Buffer{},
|
||||||
source: src,
|
|
||||||
sampleRate: sampleRate,
|
sampleRate: sampleRate,
|
||||||
alFormat: alFormat(channelNum, bytesPerSample),
|
alFormat: alFormat(channelNum, bytesPerSample),
|
||||||
}
|
}
|
||||||
runtime.SetFinalizer(p, (*Player).Close)
|
runtime.SetFinalizer(p, (*Player).Close)
|
||||||
|
|
||||||
bs := openal.NewBuffers(maxBufferNum)
|
bs := openal.NewBuffers(maxBufferNum)
|
||||||
|
const bufferSize = 1024
|
||||||
emptyBytes := make([]byte, bufferSize)
|
emptyBytes := make([]byte, bufferSize)
|
||||||
for _, b := range bs {
|
for _, b := range bs {
|
||||||
// Note that the third argument of only the first buffer is used.
|
// Note that the third argument of only the first buffer is used.
|
||||||
b.SetData(p.alFormat, emptyBytes, int32(p.sampleRate))
|
b.SetData(p.alFormat, emptyBytes, int32(p.sampleRate))
|
||||||
p.alSource.QueueBuffer(b)
|
//p.alSource.QueueBuffer(b)
|
||||||
|
p.alBuffers = append(p.alBuffers, b)
|
||||||
}
|
}
|
||||||
p.alSource.Play()
|
p.alSource.Play()
|
||||||
return p, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
func (p *Player) Proceed(data []byte) error {
|
||||||
bufferSize = 1024
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
tmpBuffer = make([]byte, bufferSize)
|
|
||||||
tmpAlBuffers = make([]openal.Buffer, maxBufferNum)
|
|
||||||
)
|
|
||||||
|
|
||||||
func (p *Player) Proceed() error {
|
|
||||||
if err := openal.Err(); err != nil {
|
if err := openal.Err(); err != nil {
|
||||||
return fmt.Errorf("driver: starting Proceed: %v", err)
|
return fmt.Errorf("driver: starting Proceed: %v", err)
|
||||||
}
|
}
|
||||||
processedNum := p.alSource.BuffersProcessed()
|
processedNum := p.alSource.BuffersProcessed()
|
||||||
if 0 < processedNum {
|
if 0 < processedNum {
|
||||||
bufs := tmpAlBuffers[:processedNum]
|
bufs := make([]openal.Buffer, processedNum)
|
||||||
p.alSource.UnqueueBuffers(bufs)
|
p.alSource.UnqueueBuffers(bufs)
|
||||||
if err := openal.Err(); err != nil {
|
if err := openal.Err(); err != nil {
|
||||||
return fmt.Errorf("driver: UnqueueBuffers: %v", err)
|
return fmt.Errorf("driver: UnqueueBuffers: %v", err)
|
||||||
@ -118,20 +109,15 @@ func (p *Player) Proceed() error {
|
|||||||
p.alBuffers = append(p.alBuffers, bufs...)
|
p.alBuffers = append(p.alBuffers, bufs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
if 0 < len(p.alBuffers) {
|
if len(p.alBuffers) == 0 {
|
||||||
n, err := p.source.Read(tmpBuffer)
|
return errors.New("driver: p.alBuffers must > 0")
|
||||||
if 0 < n {
|
}
|
||||||
buf := p.alBuffers[0]
|
buf := p.alBuffers[0]
|
||||||
p.alBuffers = p.alBuffers[1:]
|
p.alBuffers = p.alBuffers[1:]
|
||||||
buf.SetData(p.alFormat, tmpBuffer[:n], int32(p.sampleRate))
|
buf.SetData(p.alFormat, data, int32(p.sampleRate))
|
||||||
p.alSource.QueueBuffer(buf)
|
p.alSource.QueueBuffer(buf)
|
||||||
if err := openal.Err(); err != nil {
|
if err := openal.Err(); err != nil {
|
||||||
return fmt.Errorf("driver: QueueBuffer: %v", err)
|
return fmt.Errorf("driver: QueueBuffer: %v", err)
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.alSource.State() == openal.Stopped || p.alSource.State() == openal.Initial {
|
if p.alSource.State() == openal.Stopped || p.alSource.State() == openal.Initial {
|
||||||
|
@ -27,7 +27,6 @@ import "C"
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"unsafe"
|
"unsafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -76,7 +75,6 @@ func releaseSemaphore() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Player struct {
|
type Player struct {
|
||||||
src io.Reader
|
|
||||||
out C.HWAVEOUT
|
out C.HWAVEOUT
|
||||||
buffer []byte
|
buffer []byte
|
||||||
headers []*header
|
headers []*header
|
||||||
@ -84,7 +82,7 @@ type Player struct {
|
|||||||
|
|
||||||
const bufferSize = 1024
|
const bufferSize = 1024
|
||||||
|
|
||||||
func NewPlayer(src io.Reader, sampleRate, channelNum, bytesPerSample int) (*Player, error) {
|
func NewPlayer(sampleRate, channelNum, bytesPerSample int) (*Player, error) {
|
||||||
numBlockAlign := channelNum * bytesPerSample
|
numBlockAlign := channelNum * bytesPerSample
|
||||||
f := C.WAVEFORMATEX{
|
f := C.WAVEFORMATEX{
|
||||||
wFormatTag: C.WAVE_FORMAT_PCM,
|
wFormatTag: C.WAVE_FORMAT_PCM,
|
||||||
@ -99,7 +97,6 @@ func NewPlayer(src io.Reader, sampleRate, channelNum, bytesPerSample int) (*Play
|
|||||||
return nil, fmt.Errorf("driver: waveOutOpen error: %d", err)
|
return nil, fmt.Errorf("driver: waveOutOpen error: %d", err)
|
||||||
}
|
}
|
||||||
p := &Player{
|
p := &Player{
|
||||||
src: src,
|
|
||||||
out: w,
|
out: w,
|
||||||
buffer: []byte{},
|
buffer: []byte{},
|
||||||
headers: make([]*header, numHeader),
|
headers: make([]*header, numHeader),
|
||||||
@ -114,35 +111,27 @@ func NewPlayer(src io.Reader, sampleRate, channelNum, bytesPerSample int) (*Play
|
|||||||
return p, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Player) Proceed() error {
|
func (p *Player) Proceed(data []byte) error {
|
||||||
if len(p.buffer) < bufferSize {
|
p.buffer = append(p.buffer, data...)
|
||||||
b := make([]byte, bufferSize)
|
if bufferSize > len(p.buffer) {
|
||||||
n, err := p.src.Read(b)
|
return nil
|
||||||
if 0 < n {
|
}
|
||||||
p.buffer = append(p.buffer, b[:n]...)
|
sem <- struct{}{}
|
||||||
}
|
headerToWrite := (*header)(nil)
|
||||||
if err != nil {
|
for _, h := range p.headers {
|
||||||
return err
|
// TODO: Need to check WHDR_DONE?
|
||||||
|
if h.waveHdr.dwFlags&C.WHDR_INQUEUE == 0 {
|
||||||
|
headerToWrite = h
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if bufferSize <= len(p.buffer) {
|
if headerToWrite == nil {
|
||||||
sem <- struct{}{}
|
return errors.New("driver: no available buffers")
|
||||||
headerToWrite := (*header)(nil)
|
|
||||||
for _, h := range p.headers {
|
|
||||||
// TODO: Need to check WHDR_DONE?
|
|
||||||
if h.waveHdr.dwFlags&C.WHDR_INQUEUE == 0 {
|
|
||||||
headerToWrite = h
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if headerToWrite == nil {
|
|
||||||
return errors.New("driver: no available buffers")
|
|
||||||
}
|
|
||||||
if err := headerToWrite.Write(p.out, p.buffer[:bufferSize]); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
p.buffer = p.buffer[bufferSize:]
|
|
||||||
}
|
}
|
||||||
|
if err := headerToWrite.Write(p.out, p.buffer[:bufferSize]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.buffer = p.buffer[bufferSize:]
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user