github.com/noriah/catnip@v1.8.5/input/portaudio/portaudio.go (about) 1 package portaudio 2 3 import ( 4 "context" 5 "fmt" 6 "sync" 7 8 "github.com/noriah/catnip/input" 9 "github.com/noriah/catnip/input/portaudio/portaudio" 10 "github.com/pkg/errors" 11 ) 12 13 func init() { 14 input.RegisterBackend("portaudio", &PortBackend{}) 15 } 16 17 // errors 18 var ( 19 ErrBadDevice error = errors.New("device not found") 20 ErrReadTimedOut error = errors.New("read timed out") 21 ) 22 23 // PortBackend represents the Portaudio backend. A zero-value instance is a 24 // valid instance. 25 type PortBackend struct { 26 devices []*portaudio.DeviceInfo 27 } 28 29 func (b *PortBackend) Init() error { 30 return portaudio.Initialize() 31 } 32 33 func (b *PortBackend) Close() error { 34 return portaudio.Terminate() 35 } 36 37 func (b *PortBackend) Devices() ([]input.Device, error) { 38 if b.devices == nil { 39 devices, err := portaudio.Devices() 40 if err != nil { 41 return nil, err 42 } 43 b.devices = devices 44 } 45 46 gDevices := make([]input.Device, len(b.devices)) 47 for i, device := range b.devices { 48 gDevices[i] = Device{device} 49 } 50 51 return gDevices, nil 52 } 53 54 func (b *PortBackend) DefaultDevice() (input.Device, error) { 55 defaultHost, err := portaudio.DefaultHostApi() 56 if err != nil { 57 return nil, errors.Wrap(err, "failed to get default host API") 58 } 59 60 if defaultHost.DefaultInputDevice == nil { 61 return nil, errors.New("no default input device found") 62 } 63 64 return Device{defaultHost.DefaultInputDevice}, nil 65 } 66 67 func (b *PortBackend) Start(cfg input.SessionConfig) (input.Session, error) { 68 return NewSession(cfg) 69 } 70 71 // Device represents a Portaudio device. 72 type Device struct { 73 *portaudio.DeviceInfo 74 } 75 76 func (d *Device) discard() { d.DeviceInfo = nil } 77 78 // String returns the device name. 79 func (d Device) String() string { 80 return d.Name 81 } 82 83 // SampleType is broken out because portaudio supports different types 84 type SampleType = float32 85 86 // Session is an input source that pulls from Portaudio. 87 type Session struct { 88 device Device 89 config input.SessionConfig 90 } 91 92 // NewSession creates and initializes a new Portaudio session. 93 func NewSession(config input.SessionConfig) (*Session, error) { 94 dv, ok := config.Device.(Device) 95 if !ok { 96 return nil, fmt.Errorf("device is on unknown type %T", config.Device) 97 } 98 99 // Free up the device inside the config. 100 config.Device = nil 101 102 return &Session{ 103 dv, 104 config, 105 }, nil 106 } 107 108 func (s *Session) Start(ctx context.Context, dst [][]input.Sample, kickChan chan bool, mu *sync.Mutex) error { 109 if !input.EnsureBufferLen(s.config, dst) { 110 return errors.New("invalid dst length given") 111 } 112 113 param := portaudio.StreamParameters{ 114 Input: portaudio.StreamDeviceParameters{ 115 Device: s.device.DeviceInfo, 116 Latency: s.device.DefaultLowInputLatency, 117 Channels: s.config.FrameSize, 118 }, 119 SampleRate: s.config.SampleRate, 120 FramesPerBuffer: s.config.SampleSize, 121 Flags: portaudio.ClipOff | portaudio.DitherOff, 122 } 123 124 frameSize := s.config.FrameSize 125 samples := s.config.SampleSize * frameSize 126 127 // Source buffer in a different format than what we want (dst). 128 src := make([]SampleType, samples) 129 130 stream, err := portaudio.OpenStream(param, src) 131 if err != nil { 132 return errors.Wrap(err, "failed to open stream") 133 } 134 s.device.discard() 135 defer stream.Close() 136 137 if err := stream.Start(); err != nil { 138 return errors.Wrap(err, "failed to start stream") 139 } 140 defer stream.Stop() 141 142 for { 143 144 // Ignore overflow in case the processing is too slow. 145 if err := stream.Read(); err != nil && err != portaudio.InputOverflowed { 146 return errors.Wrap(err, "failed to read stream") 147 } 148 149 mu.Lock() 150 for x := 0; x < samples; x++ { 151 dst[x%frameSize][x/frameSize] = input.Sample(src[x]) 152 } 153 mu.Unlock() 154 155 loop := true 156 for { 157 select { 158 case <-ctx.Done(): 159 return ctx.Err() 160 case kickChan <- true: 161 loop = false 162 default: 163 } 164 165 if ready, _ := stream.AvailableToRead(); !loop || ready >= samples { 166 break 167 } 168 } 169 } 170 }