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

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