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 }