diff --git a/CHANGELOG.md b/CHANGELOG.md index da5a957..ad50d29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +## v0.4.1 2026-03-23 +Changes: +* Reduce streamed audio packetization from 1 second to smaller realtime intervals for faster partial transcripts and SafeToStopAudio handling. +* Add GetLPCMStreamInfo to centralize LPCM chunk-size and streaming-interval calculation. +* Validate LPCM inputs more strictly (numChans, bitDepth, sampleRate, and targetStreamIntervalMs). +* Refactor the example streamer to use the shared LPCM stream info helper. +* Add isolated table-driven tests covering expected chunk sizes and streaming intervals for multiple sample rates and intervals, plus invalid-input cases. +* Replaced the timer.Sleep() to ticker in audio streaming example - to avoid any drifts over time. This change will also help prevent writing any chunks after SafeToStopAudio has been received. +* Other upgrades: + * Update go version to 1.26 + * Remove usage of deprecated io/ioutil + * Remove usage of deprecated github.com/pkg/errors + * Replace gotest.tools/assert with github.com/stretchr/testify/assert + ## v0.3.4 2019-07-17 Features: * Pass the SafeToStopAudio flag recieved from the server with the PartialTranscript (See diff --git a/README.md b/README.md index 94d63dd..71f66b2 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ The SDK allows you to make voice and text queries to the Houndify API. The SDK c ## Requirements -- Go v1.8+ +- Go v1.26+ - Houndify account available from [Houndify.com](https://www.houndify.com) ## Installing diff --git a/example/example.go b/example/example.go index a70274e..d0b7889 100644 --- a/example/example.go +++ b/example/example.go @@ -8,16 +8,16 @@ import ( "crypto/tls" "flag" "fmt" - "github.com/go-audio/wav" - houndify "github.com/soundhound/houndify-sdk-go" "io" - "io/ioutil" "log" "net/http/httptrace" "net/textproto" "os" "strings" "time" + + "github.com/go-audio/wav" + houndify "github.com/soundhound/houndify-sdk-go" ) const ( @@ -78,7 +78,7 @@ func main() { case *voiceFlag != "" && !*streamFlag: // voice query audioFilePath := *voiceFlag - fileContents, err := ioutil.ReadFile(audioFilePath) + fileContents, err := os.ReadFile(audioFilePath) if err != nil { log.Fatalf("failed to read contents of file %q, err: %v", audioFilePath, err) } @@ -168,28 +168,35 @@ func main() { } } -// Stream an audio file to the server. This example demonstrates streaming a wav file, -// however this could easily be changed to stream audio from a microphone or something. -// Basically it just writes data from a buffer to the Request body every 1 second. The -// advantage of how golang has the http.Request's Body field is it's a Reader, so using -// io.Pipe() you can actually write any data into it. That means any stream of WAV data -// can just be piped in, and the requests will be made. +// Streams audio to the server using a WAV file as the source. While this example +// uses a file, the same pattern can be used for other sources like a microphone. // -// This function also demonstrates how you can use the SafeToStopAudio flag to know when -// the server has all the data it needs. +// Audio is sent in frame-aligned chunks at a realtime interval to better reflect +// live streaming behavior. +// +// The request body is backed by an io.Pipe, which allows arbitrary data to be +// written as a stream. This makes it easy to feed any WAV audio source directly +// into the request as it becomes available. +// +// The function also shows how to use the SafeToStopAudio signal to determine when +// the server has received enough audio and no more data needs to be sent. func StreamAudio(client houndify.Client, fname, uid string) { f, err := os.Open(fname) - defer f.Close() if err != nil { log.Fatalf("failed to read contents of file %q, err: %v\n", fname, err) } + defer f.Close() // Read WAV file data, determine bytes per second d := wav.NewDecoder(f) d.ReadInfo() - // Use 1 second chunks - bps := int(d.AvgBytesPerSec) * 1 + targetStreamingIntervalMs := 20 + streamInfo, err := houndify.GetLPCMStreamInfo(int(d.NumChans), + int(d.BitDepth), int(d.SampleRate), targetStreamingIntervalMs) + if err != nil { + log.Fatalf("failed to get LPCM chunk info: %v", err) + } // Build pipe that lets us write into the io.Reader that is in the request rp, wp := io.Pipe() @@ -200,36 +207,46 @@ func StreamAudio(client houndify.Client, fname, uid string) { RequestID: createRequestID(), } - // Start the function to write 1 second of data per 1 real second, by using a buffer - // that is the size of 1 second of data. Note that using the .Read() function results + // Start the function to stream audio in realtime + // Note that using the .Read() function results // in the header portion of the file not being read. We have to use the ReadAt() // function to specify starting at the very first position of the actual file, or the // header isn't read. - var loc int64 = 0 - buf := make([]byte, bps) done := make(chan bool) go func(wp *io.PipeWriter) { defer wp.Close() + var ( + loc int64 = 0 + buf = make([]byte, streamInfo.ChunkSize()) + ticker = time.NewTicker(streamInfo.StreamingInterval()) + ) + defer ticker.Stop() + for { select { case <-done: - //fmt.Println("Exiting write loop") + fmt.Println("Context received done, exiting write loop") return - default: + + case <-ticker.C: + n, err := f.ReadAt(buf, loc) - loc += int64(n) - // At the EOF, the buffer will still have bytes read into it, have to write - // those out before breaking the loop - if err == io.EOF { + if n > 0 { + loc += int64(n) + // Write the amount of bytes that were read in wp.Write(buf[:n]) - return } - // Write the amount of bytes that were read in - wp.Write(buf[:n]) - time.Sleep(time.Duration(1) * time.Second) + if err != nil { + if err != io.EOF { + // handle error + } else { + fmt.Println("Reached end of file") + } + return + } } } }(wp) @@ -284,7 +301,7 @@ func derefOrFetchFromEnv(strPtr *string, envKey string) string { } func getDefaultClientTrace() *httptrace.ClientTrace { - traceLogger := log.New(os.Stdout, "[httptrace] ", log.Ltime | log.Lmicroseconds) + traceLogger := log.New(os.Stdout, "[httptrace] ", log.Ltime|log.Lmicroseconds) trace := &httptrace.ClientTrace{ GotConn: func(info httptrace.GotConnInfo) { traceLogger.Println("GotConn: ", info) diff --git a/go.mod b/go.mod index dca35f1..20d120b 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,16 @@ module github.com/soundhound/houndify-sdk-go -go 1.12 +go 1.26 require ( - github.com/go-audio/wav v1.0.0 - github.com/google/go-cmp v0.3.0 // indirect - github.com/pkg/errors v0.8.1 - gotest.tools v2.2.0+incompatible + github.com/go-audio/wav v1.1.0 + github.com/stretchr/testify v1.11.1 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-audio/audio v1.0.0 // indirect + github.com/go-audio/riff v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 78c08ae..ad58559 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,16 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-audio/audio v1.0.0 h1:zS9vebldgbQqktK4H0lUqWrG8P0NxCJVqcj7ZpNnwd4= github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs= github.com/go-audio/riff v1.0.0 h1:d8iCGbDvox9BfLagY94fBynxSPHO80LmZCaOsmKxokA= github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498= -github.com/go-audio/wav v1.0.0 h1:WdSGLhtyud6bof6XHL28xKeCQRzCV06pOFo3LZsFdyE= -github.com/go-audio/wav v1.0.0/go.mod h1:3yoReyQOsiARkvPl3ERCi8JFjihzG6WhjYpZCf5zAWE= -github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= -gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +github.com/go-audio/wav v1.1.0 h1:jQgLtbqBzY7G+BM8fXF7AHUk1uHUviWS4X39d5rsL2g= +github.com/go-audio/wav v1.1.0/go.mod h1:mpe9qfwbScEbkd8uybLuIpTgHyrISw/OTuvjUW2iGtE= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/houndify_client.go b/houndify_client.go index d9ed68d..ebd3c79 100644 --- a/houndify_client.go +++ b/houndify_client.go @@ -3,10 +3,9 @@ package houndify import ( "bufio" "encoding/json" + "errors" "fmt" - "github.com/pkg/errors" "io" - "io/ioutil" "net/http" "strconv" "strings" @@ -109,7 +108,7 @@ func (c *Client) TextSearch(textReq TextRequest) (string, error) { return "", errors.New("failed to successfully run request: " + err.Error()) } - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { return "", errors.New("failed to read body: " + err.Error()) } @@ -131,7 +130,7 @@ func (c *Client) TextSearch(textReq TextRequest) (string, error) { if c.enableConversationState { newConvState, err := parseConversationState(bodyStr) if err != nil { - return bodyStr, errors.Wrap(err, "unable to parse new conversation state from response") + return bodyStr, fmt.Errorf("unable to parse new conversation state from response: %w", err) } c.conversationState = newConvState } @@ -178,7 +177,7 @@ func (c *Client) VoiceSearch(voiceReq VoiceRequest, partialTranscriptChan chan P if err != nil { return "", err } - req.Body = ioutil.NopCloser(voiceReq.AudioStream) + req.Body = io.NopCloser(voiceReq.AudioStream) if c.HttpClient == nil { c.HttpClient = &http.Client{} @@ -262,7 +261,7 @@ func (c *Client) VoiceSearch(voiceReq VoiceRequest, partialTranscriptChan chan P if c.enableConversationState { newConvState, err := parseConversationState(bodyStr) if err != nil { - return bodyStr, errors.Wrap(err, "unable to parse new conversation state from response") + return bodyStr, fmt.Errorf("unable to parse new conversation state from response: %w", err) } c.conversationState = newConvState } diff --git a/lpcm_stream_info.go b/lpcm_stream_info.go new file mode 100644 index 0000000..50c4134 --- /dev/null +++ b/lpcm_stream_info.go @@ -0,0 +1,125 @@ +package houndify + +import ( + "fmt" + "time" +) + +type LPCMStreamInfo struct { + numChans int + bitDepth int + sampleRate int + targetStreamingIntervalMs int + + // Calculated output fields: + idealChunkSize int // raw, unaligned byte count for the target streaming interval. + + // The client should stream one chunk of chunkSize bytes every streamingInterval. + chunkSize int // frame-aligned byte count that should be streamed each tick. + streamingInterval time.Duration // the duration represented by chunkSize +} + +func (info *LPCMStreamInfo) NumChans() int { + return info.numChans +} + +func (info *LPCMStreamInfo) BitDepth() int { + return info.bitDepth +} + +func (info *LPCMStreamInfo) SampleRate() int { + return info.sampleRate +} + +func (info *LPCMStreamInfo) TargetStreamingIntervalMs() int { + return info.targetStreamingIntervalMs +} + +func (info *LPCMStreamInfo) IdealChunkSize() int { + return info.idealChunkSize +} + +func (info *LPCMStreamInfo) ChunkSize() int { + return info.chunkSize +} + +func (info *LPCMStreamInfo) StreamingInterval() time.Duration { + return info.streamingInterval +} + +// GetLPCMStreamInfo computes the appropriate chunk size and streaming interval +// for streaming linear PCM audio data. +// +// It takes audio parameters (number of channels, bit depth, target streaming interval in +// milliseconds, and sample rate) and calculates the frame-aligned chunk size and the actual +// streaming interval implied by that aligned chunk size. The resulting interval is chosen +// to be as close as possible to the requested target and will often match it exactly. +// +// The function performs three steps: +// 1. Calculates the ideal number of bytes needed to represent the target streaming interval +// 2. Aligns the byte count to full audio frames to ensure valid audio boundaries +// 3. Derives the actual streaming interval from the aligned byte count +// +// Parameters: +// - numChans: Number of audio channels +// - bitDepth: Bit depth of each audio sample (e.g., 8, 16, 24, 32) +// - sampleRate: Sample rate in Hz (e.g., 16000, 44100) +// - targetStreamingIntervalMs: Target streaming interval in milliseconds +// +// Returns an LPCMStreamInfo containing both the source audio metadata and the +// calculated streaming values: +// - numChans: the number of audio channels in the source stream +// - bitDepth: the bits per sample for each channel +// - sampleRate: the sampling rate in Hz +// - targetStreamingIntervalMs: the requested streaming cadence in milliseconds +// - idealChunkSize: the exact byte count for the requested interval before frame alignment +// - chunkSize: the frame-aligned byte count to write for each chunk +// - streamingInterval: the duration represented by actualChunkSize +func GetLPCMStreamInfo( + numChans int, + bitDepth int, + sampleRate int, + targetStreamingIntervalMs int, +) (*LPCMStreamInfo, error) { + + if numChans < 1 { + return nil, + fmt.Errorf("invalid input: numChans must be >= 1, got %d", numChans) + } + if bitDepth < 8 || (bitDepth%8 != 0) { + return nil, + fmt.Errorf("invalid input: bitDepth must be >= 8 and multiple of 8, got %d", bitDepth) + } + if sampleRate < 8000 { + return nil, + fmt.Errorf("invalid input: sampleRate must be >= 8000, got %d", sampleRate) + } + if targetStreamingIntervalMs < 1 { + return nil, + fmt.Errorf("invalid input: targetStreamingIntervalMs must be >= 1, got %d", + targetStreamingIntervalMs) + } + + bytesPerFrame := numChans * (bitDepth / 8) + bytesPerSecond := sampleRate * bytesPerFrame + + // Step 1: ideal (non-aligned) byte size + idealChunkSize := (bytesPerSecond * targetStreamingIntervalMs) / 1000 + + // Step 2: align to full frames + chunkSize := (idealChunkSize / bytesPerFrame) * bytesPerFrame + + // Step 3: derive the actual streaming interval from bytes + streamingInterval := (time.Duration(chunkSize) * time.Second) / time.Duration(bytesPerSecond) + + return &LPCMStreamInfo{ + numChans: numChans, + bitDepth: bitDepth, + sampleRate: sampleRate, + targetStreamingIntervalMs: targetStreamingIntervalMs, + + idealChunkSize: idealChunkSize, + chunkSize: chunkSize, + streamingInterval: streamingInterval, + }, nil +} diff --git a/lpcm_stream_info_test.go b/lpcm_stream_info_test.go new file mode 100644 index 0000000..70acf84 --- /dev/null +++ b/lpcm_stream_info_test.go @@ -0,0 +1,313 @@ +package houndify_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + + houndify "github.com/soundhound/houndify-sdk-go" +) + +func TestGetLPCMStreamInfo(t *testing.T) { + const ( + numChans = 1 + bitDepth = 16 + ) + + testCases := []struct { + name string + targetStreamingIntervalMs int + sampleRate int + idealChunkSize int + chunkSize int + streamingInterval time.Duration + }{ + { + name: "20ms_8000hz", + targetStreamingIntervalMs: 20, + sampleRate: 8000, + idealChunkSize: 320, + chunkSize: 320, + streamingInterval: 20 * time.Millisecond, + }, + { + name: "20ms_11025hz", + targetStreamingIntervalMs: 20, + sampleRate: 11025, + idealChunkSize: 441, + chunkSize: 440, + streamingInterval: 19954648 * time.Nanosecond, + }, + { + name: "20ms_16000hz", + targetStreamingIntervalMs: 20, + sampleRate: 16000, + idealChunkSize: 640, + chunkSize: 640, + streamingInterval: 20 * time.Millisecond, + }, + { + name: "20ms_20500hz", + targetStreamingIntervalMs: 20, + sampleRate: 20500, + idealChunkSize: 820, + chunkSize: 820, + streamingInterval: 20 * time.Millisecond, + }, + { + name: "20ms_21000hz", + targetStreamingIntervalMs: 20, + sampleRate: 21000, + idealChunkSize: 840, + chunkSize: 840, + streamingInterval: 20 * time.Millisecond, + }, + { + name: "20ms_32000hz", + targetStreamingIntervalMs: 20, + sampleRate: 32000, + idealChunkSize: 1280, + chunkSize: 1280, + streamingInterval: 20 * time.Millisecond, + }, + { + name: "20ms_44100hz", + targetStreamingIntervalMs: 20, + sampleRate: 44100, + idealChunkSize: 1764, + chunkSize: 1764, + streamingInterval: 20 * time.Millisecond, + }, + { + name: "20ms_48000hz", + targetStreamingIntervalMs: 20, + sampleRate: 48000, + idealChunkSize: 1920, + chunkSize: 1920, + streamingInterval: 20 * time.Millisecond, + }, + { + name: "30ms_8000hz", + targetStreamingIntervalMs: 30, + sampleRate: 8000, + idealChunkSize: 480, + chunkSize: 480, + streamingInterval: 30 * time.Millisecond, + }, + { + name: "30ms_11025hz", + targetStreamingIntervalMs: 30, + sampleRate: 11025, + idealChunkSize: 661, + chunkSize: 660, + streamingInterval: 29931972 * time.Nanosecond, + }, + { + name: "30ms_16000hz", + targetStreamingIntervalMs: 30, + sampleRate: 16000, + idealChunkSize: 960, + chunkSize: 960, + streamingInterval: 30 * time.Millisecond, + }, + { + name: "30ms_20500hz", + targetStreamingIntervalMs: 30, + sampleRate: 20500, + idealChunkSize: 1230, + chunkSize: 1230, + streamingInterval: 30 * time.Millisecond, + }, + { + name: "30ms_21000hz", + targetStreamingIntervalMs: 30, + sampleRate: 21000, + idealChunkSize: 1260, + chunkSize: 1260, + streamingInterval: 30 * time.Millisecond, + }, + { + name: "30ms_32000hz", + targetStreamingIntervalMs: 30, + sampleRate: 32000, + idealChunkSize: 1920, + chunkSize: 1920, + streamingInterval: 30 * time.Millisecond, + }, + { + name: "30ms_44100hz", + targetStreamingIntervalMs: 30, + sampleRate: 44100, + idealChunkSize: 2646, + chunkSize: 2646, + streamingInterval: 30 * time.Millisecond, + }, + { + name: "30ms_48000hz", + targetStreamingIntervalMs: 30, + sampleRate: 48000, + idealChunkSize: 2880, + chunkSize: 2880, + streamingInterval: 30 * time.Millisecond, + }, + { + name: "33ms_8000hz", + targetStreamingIntervalMs: 33, + sampleRate: 8000, + idealChunkSize: 528, + chunkSize: 528, + streamingInterval: 33 * time.Millisecond, + }, + { + name: "33ms_11025hz", + targetStreamingIntervalMs: 33, + sampleRate: 11025, + idealChunkSize: 727, + chunkSize: 726, + streamingInterval: 32925170 * time.Nanosecond, + }, + { + name: "33ms_16000hz", + targetStreamingIntervalMs: 33, + sampleRate: 16000, + idealChunkSize: 1056, + chunkSize: 1056, + streamingInterval: 33 * time.Millisecond, + }, + { + name: "33ms_20500hz", + targetStreamingIntervalMs: 33, + sampleRate: 20500, + idealChunkSize: 1353, + chunkSize: 1352, + streamingInterval: 32975609 * time.Nanosecond, + }, + { + name: "33ms_21000hz", + targetStreamingIntervalMs: 33, + sampleRate: 21000, + idealChunkSize: 1386, + chunkSize: 1386, + streamingInterval: 33 * time.Millisecond, + }, + { + name: "33ms_32000hz", + targetStreamingIntervalMs: 33, + sampleRate: 32000, + idealChunkSize: 2112, + chunkSize: 2112, + streamingInterval: 33 * time.Millisecond, + }, + { + name: "33ms_44100hz", + targetStreamingIntervalMs: 33, + sampleRate: 44100, + idealChunkSize: 2910, + chunkSize: 2910, + streamingInterval: 32993197 * time.Nanosecond, + }, + { + name: "33ms_48000hz", + targetStreamingIntervalMs: 33, + sampleRate: 48000, + idealChunkSize: 3168, + chunkSize: 3168, + streamingInterval: 33 * time.Millisecond, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + chunkInfo, err := houndify.GetLPCMStreamInfo( + numChans, + bitDepth, + tc.sampleRate, + tc.targetStreamingIntervalMs, + ) + assert.NoError(t, err) + + assert.Equal(t, chunkInfo.NumChans(), numChans) + assert.Equal(t, chunkInfo.BitDepth(), bitDepth) + assert.Equal(t, chunkInfo.SampleRate(), tc.sampleRate) + assert.Equal(t, chunkInfo.TargetStreamingIntervalMs(), tc.targetStreamingIntervalMs) + assert.Equal(t, chunkInfo.IdealChunkSize(), tc.idealChunkSize) + assert.Equal(t, chunkInfo.ChunkSize(), tc.chunkSize) + assert.Equal(t, chunkInfo.StreamingInterval(), tc.streamingInterval) + }) + } +} + +func TestGetLPCMStreamInfoRejectsInvalidInput(t *testing.T) { + testCases := []struct { + name string + numChans int + bitDepth int + sampleRate int + targetStreamingIntervalMs int + errSubstring string + }{ + { + name: "zero_channels", + numChans: 0, + bitDepth: 16, + sampleRate: 16000, + targetStreamingIntervalMs: 20, + errSubstring: "invalid input: numChans must be >= 1", + }, + { + name: "bit_depth_too_small", + numChans: 1, + bitDepth: 0, + sampleRate: 16000, + targetStreamingIntervalMs: 20, + errSubstring: "invalid input: bitDepth must be >= 8 and multiple of 8", + }, + { + name: "bit_depth_not_multiple_of_8", + numChans: 1, + bitDepth: 10, + sampleRate: 16000, + targetStreamingIntervalMs: 20, + errSubstring: "invalid input: bitDepth must be >= 8 and multiple of 8", + }, + { + name: "zero_sample_rate", + numChans: 1, + bitDepth: 16, + sampleRate: 0, + targetStreamingIntervalMs: 20, + errSubstring: "invalid input: sampleRate must be >= 8000", + }, + { + name: "sample_rate_too_small", + numChans: 1, + bitDepth: 16, + sampleRate: 1000, + targetStreamingIntervalMs: 20, + errSubstring: "invalid input: sampleRate must be >= 8000", + }, + { + name: "zero_target_interval", + numChans: 1, + bitDepth: 16, + sampleRate: 16000, + targetStreamingIntervalMs: 0, + errSubstring: "invalid input: targetStreamingIntervalMs must be >= 1", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + chunkInfo, err := houndify.GetLPCMStreamInfo( + tc.numChans, + tc.bitDepth, + tc.sampleRate, + tc.targetStreamingIntervalMs, + ) + assert.Error(t, err) + assert.Nil(t, chunkInfo) + assert.ErrorContains(t, err, tc.errSubstring) + }) + } +} diff --git a/request.go b/request.go index 66bdddf..b4af38d 100644 --- a/request.go +++ b/request.go @@ -6,7 +6,6 @@ import ( "encoding/json" "errors" "io" - "io/ioutil" "net/http" "net/url" "strconv" @@ -138,7 +137,7 @@ func BuildRequest(houndReq requestable, c Client) (*http.Request, error) { if err != nil { return nil, errors.New("failed to create request info: " + err.Error()) } - req.Body = ioutil.NopCloser(bytes.NewBuffer(requestInfoJSON)) + req.Body = io.NopCloser(bytes.NewBuffer(requestInfoJSON)) } return req, nil } diff --git a/request_info.go b/request_info.go index 9f23916..625dd8a 100644 --- a/request_info.go +++ b/request_info.go @@ -16,7 +16,7 @@ func createRequestInfo(clientID, requestID string, timeStamp int64, extraFields reqInfo["ClientID"] = clientID reqInfo["RequestID"] = requestID reqInfo["SDK"] = "Go" - reqInfo["SDKVersion"] = "0.1" + reqInfo["SDKVersion"] = "0.2" reqInfo["PartialTranscriptsDesired"] = true reqInfo["ObjectByteCountPrefix"] = true return reqInfo, nil diff --git a/request_test.go b/request_test.go index 97de921..8d68174 100644 --- a/request_test.go +++ b/request_test.go @@ -2,11 +2,12 @@ package houndify_test import ( "bytes" - . "github.com/soundhound/houndify-sdk-go" - "gotest.tools/assert" - "io/ioutil" + "io" "net/http" "testing" + + . "github.com/soundhound/houndify-sdk-go" + "github.com/stretchr/testify/assert" ) type RoundTripFunc func(req *http.Request) *http.Response @@ -66,14 +67,14 @@ func TestNewTextRequest(t *testing.T) { assert.Equal(t, req.URL.String(), "http://test.com/v1/text?query=what%20is%20the%20time") return &http.Response{ StatusCode: 200, - Body: ioutil.NopCloser(bytes.NewBufferString(`No clue`)), + Body: io.NopCloser(bytes.NewBufferString(`No clue`)), Header: make(http.Header), } }) textReq := NewTestTextRequest() req, err := textReq.NewRequest() - assert.NilError(t, err) + assert.NoError(t, err) mockClient.Do(req) } @@ -85,14 +86,14 @@ func TestNewVoiceRequest(t *testing.T) { assert.Equal(t, req.URL.String(), "http://test.com/v1/voice") return &http.Response{ StatusCode: 200, - Body: ioutil.NopCloser(bytes.NewBufferString(`No clue`)), + Body: io.NopCloser(bytes.NewBufferString(`No clue`)), Header: make(http.Header), } }) voiceReq := NewTestVoiceRequest() req, err := voiceReq.NewRequest() - assert.NilError(t, err) + assert.NoError(t, err) mockClient.Do(req) } @@ -101,8 +102,8 @@ func TestNewVoiceRequest(t *testing.T) { // - User Agent is set properly // - Headers all exist that are set // - TODO: -// - RequestInfo verification -// - Find way to mock Auth stuff so dynamic auth headers (they change with time etc) +// - RequestInfo verification +// - Find way to mock Auth stuff so dynamic auth headers (they change with time etc) func TestBuildTextRequest(t *testing.T) { var expectedVals = map[string]string{ @@ -124,6 +125,6 @@ func TestBuildTextRequest(t *testing.T) { textReq := NewTestTextRequest() houndifyClient := NewTestHoundifyClient(mockClient) req, err := BuildRequest(&textReq, houndifyClient) - assert.NilError(t, err) + assert.NoError(t, err) mockClient.Do(req) } diff --git a/server_response.go b/server_response.go index ec0ac5a..7a721a1 100644 --- a/server_response.go +++ b/server_response.go @@ -2,8 +2,8 @@ package houndify import ( "encoding/json" + "errors" "fmt" - "github.com/pkg/errors" "strings" )