github.com/noriah/catnip@v1.8.5/input/stdinput/stdinput.go (about)

     1  package stdinput
     2  
     3  import (
     4  	"context"
     5  	"encoding/binary"
     6  	"io"
     7  	"math"
     8  	"os"
     9  	"sync"
    10  	"time"
    11  	_ "unsafe"
    12  
    13  	"github.com/noriah/catnip/input"
    14  	"github.com/pkg/errors"
    15  )
    16  
    17  func init() {
    18  	input.RegisterBackend("stdin", StdinBackend{})
    19  }
    20  
    21  type StdinBackend struct{}
    22  
    23  func (b StdinBackend) Init() error {
    24  	return nil
    25  }
    26  
    27  func (b StdinBackend) Close() error {
    28  	return nil
    29  }
    30  
    31  func (b StdinBackend) Devices() ([]input.Device, error) {
    32  	return []input.Device{StdInputDevice{}}, nil
    33  }
    34  
    35  func (b StdinBackend) DefaultDevice() (input.Device, error) {
    36  	return StdInputDevice{}, nil
    37  }
    38  
    39  func (b StdinBackend) Start(config input.SessionConfig) (input.Session, error) {
    40  	return NewStdinSession(config), nil
    41  }
    42  
    43  type StdInputDevice struct{}
    44  
    45  func (d StdInputDevice) String() string {
    46  	return "stdin"
    47  }
    48  
    49  type Session struct {
    50  	cfg     input.SessionConfig
    51  	samples int
    52  	// maligned.
    53  	f32mode bool
    54  }
    55  
    56  func NewStdinSession(cfg input.SessionConfig) *Session {
    57  	return &Session{
    58  		cfg:     cfg,
    59  		f32mode: true,
    60  		samples: cfg.SampleSize * cfg.FrameSize,
    61  	}
    62  }
    63  
    64  func (s *Session) Start(ctx context.Context, dst [][]input.Sample, kickChan chan bool, mu *sync.Mutex) error {
    65  	if !input.EnsureBufferLen(s.cfg, dst) {
    66  		return errors.New("invalid dst length given")
    67  	}
    68  
    69  	// if initial_flags, err := Fcntl(int(uintptr(syscall.Stdin)), syscall.F_GETFL, 0); err == nil {
    70  	// 	initial_flags |= syscall.O_NONBLOCK
    71  	// 	if _, err := Fcntl(int(uintptr(syscall.Stdin)), syscall.F_SETFL, initial_flags); err != nil {
    72  	// 		return errors.Wrap(err, "failed to set non block flag.")
    73  	// 	}
    74  	// } else {
    75  	// 	return errors.Wrap(err, "failed to set non block flag.")
    76  	// }
    77  
    78  	o := os.Stdin
    79  	defer o.Close()
    80  
    81  	framesz := s.cfg.FrameSize
    82  	reader := floatReader{
    83  		order: binary.LittleEndian,
    84  		f64:   !s.f32mode,
    85  	}
    86  
    87  	bufsz := s.samples
    88  	if !s.f32mode {
    89  		bufsz *= 2
    90  	}
    91  
    92  	raw := make([]byte, bufsz*4)
    93  
    94  	// We double this as a workaround because sampleDuration is less than the
    95  	// actual time that ReadFull blocks for some reason, probably because the
    96  	// process decides to discard audio when it overflows.
    97  	sampleDuration := time.Duration(
    98  		float64(s.cfg.SampleSize) / s.cfg.SampleRate * float64(time.Second))
    99  	// We also keep track of whether the deadline was hit once so we can half
   100  	// the sample duration. This smooths out the jitter.
   101  	var readExpired bool
   102  
   103  	// timer := time.NewTimer(time.Millisecond * 100)
   104  
   105  	for {
   106  		// Set us a read deadline. If the deadline is reached, we'll write zeros
   107  		// to the buffer.
   108  		timeout := sampleDuration
   109  		if !readExpired {
   110  			timeout *= 2
   111  		}
   112  		// if err := o.SetReadDeadline(time.Now().Add(timeout)); err != nil {
   113  		// 	return errors.Wrap(err, "failed to set read deadline")
   114  		// }
   115  
   116  		_, err := io.ReadFull(o, raw)
   117  		if err != nil {
   118  			switch {
   119  			case errors.Is(err, io.EOF):
   120  				return nil
   121  			case errors.Is(err, os.ErrDeadlineExceeded):
   122  				readExpired = true
   123  			default:
   124  				return err
   125  			}
   126  		} else {
   127  			readExpired = false
   128  		}
   129  
   130  		if readExpired {
   131  			mu.Lock()
   132  			// We can write directly to dst just so we can avoid parsing zero
   133  			// bytes to floats.
   134  			for _, buf := range dst {
   135  				// Go should optimize this to a memclr.
   136  				for i := range buf {
   137  					buf[i] = 0
   138  				}
   139  			}
   140  			mu.Unlock()
   141  		} else {
   142  			reader.reset(raw)
   143  			mu.Lock()
   144  			for n := 0; n < s.samples; n++ {
   145  				dst[n%framesz][n/framesz] = reader.next()
   146  			}
   147  			mu.Unlock()
   148  		}
   149  
   150  		// Signal that we've written to dst.
   151  		select {
   152  		case <-ctx.Done():
   153  			return ctx.Err()
   154  		case kickChan <- true:
   155  		}
   156  	}
   157  }
   158  
   159  type floatReader struct {
   160  	order binary.ByteOrder
   161  	buf   []byte
   162  	f64   bool
   163  }
   164  
   165  func (f *floatReader) reset(b []byte) {
   166  	f.buf = b
   167  }
   168  
   169  func (f *floatReader) next() float64 {
   170  	if f.f64 {
   171  		b := f.buf[:8]
   172  		f.buf = f.buf[8:]
   173  		return math.Float64frombits(f.order.Uint64(b))
   174  	}
   175  
   176  	b := f.buf[:4]
   177  	f.buf = f.buf[4:]
   178  	return float64(math.Float32frombits(f.order.Uint32(b)))
   179  }