Skip to content

Commit 395a120

Browse files
xaionaro@dx.centerxaionaro@dx.center
authored andcommitted
feat: add workflow commands for camera capture, audio record/play, sensor read
Hand-written workflow commands that chain multiple NDK API calls: - camera capture: ImageReader -> ANativeWindow -> Camera2 pipeline -> raw frames - camera list-details: enumerate all cameras with characteristics - audio record: StreamBuilder -> Input stream -> Read() -> file - audio play: file -> StreamBuilder -> Output stream -> Write() - sensor read: probe sensor by type, print properties - trace section: begin a named trace section
1 parent a79a68e commit 395a120

4 files changed

Lines changed: 512 additions & 0 deletions

File tree

cmd/ndkcli/audio_workflow.go

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"os"
7+
"time"
8+
9+
"github.com/spf13/cobra"
10+
"github.com/xaionaro-go/ndk/audio"
11+
)
12+
13+
var audioRecordCmd = &cobra.Command{
14+
Use: "record",
15+
Short: "Record raw PCM16 audio to a file",
16+
RunE: func(cmd *cobra.Command, args []string) (_err error) {
17+
output, _ := cmd.Flags().GetString("output")
18+
duration, _ := cmd.Flags().GetDuration("duration")
19+
sampleRate, _ := cmd.Flags().GetInt32("sample-rate")
20+
channels, _ := cmd.Flags().GetInt32("channels")
21+
22+
builder, err := audio.NewStreamBuilder()
23+
if err != nil {
24+
return fmt.Errorf("creating stream builder: %w", err)
25+
}
26+
defer builder.Close()
27+
28+
builder.
29+
SetDirection(audio.Input).
30+
SetSampleRate(sampleRate).
31+
SetChannelCount(channels).
32+
SetFormat(audio.PcmI16)
33+
34+
stream, err := builder.Open()
35+
if err != nil {
36+
return fmt.Errorf("opening stream: %w", err)
37+
}
38+
defer func() {
39+
if closeErr := stream.Close(); closeErr != nil && _err == nil {
40+
_err = fmt.Errorf("closing stream: %w", closeErr)
41+
}
42+
}()
43+
44+
if err := stream.Start(); err != nil {
45+
return fmt.Errorf("starting stream: %w", err)
46+
}
47+
defer func() {
48+
if stopErr := stream.Stop(); stopErr != nil && _err == nil {
49+
_err = fmt.Errorf("stopping stream: %w", stopErr)
50+
}
51+
}()
52+
53+
f, err := os.Create(output)
54+
if err != nil {
55+
return fmt.Errorf("creating output file: %w", err)
56+
}
57+
defer f.Close()
58+
59+
framesPerBurst := stream.FramesPerBurst()
60+
// PCM16: 2 bytes per sample per channel
61+
bytesPerFrame := channels * 2
62+
buf := make([]byte, framesPerBurst*bytesPerFrame)
63+
timeout := 100 * time.Millisecond
64+
65+
deadline := time.Now().Add(duration)
66+
var totalFrames int64
67+
for time.Now().Before(deadline) {
68+
framesRead, err := stream.Read(buf, framesPerBurst, timeout)
69+
if err != nil {
70+
return fmt.Errorf("reading from stream: %w", err)
71+
}
72+
if framesRead > 0 {
73+
bytesRead := framesRead * bytesPerFrame
74+
if _, err := f.Write(buf[:bytesRead]); err != nil {
75+
return fmt.Errorf("writing to file: %w", err)
76+
}
77+
totalFrames += int64(framesRead)
78+
}
79+
}
80+
81+
fmt.Printf("recorded %d frames (%d bytes) to %s\n", totalFrames, totalFrames*int64(bytesPerFrame), output)
82+
return nil
83+
},
84+
}
85+
86+
var audioPlayCmd = &cobra.Command{
87+
Use: "play",
88+
Short: "Play raw PCM16 audio from a file",
89+
RunE: func(cmd *cobra.Command, args []string) (_err error) {
90+
input, _ := cmd.Flags().GetString("input")
91+
sampleRate, _ := cmd.Flags().GetInt32("sample-rate")
92+
channels, _ := cmd.Flags().GetInt32("channels")
93+
94+
builder, err := audio.NewStreamBuilder()
95+
if err != nil {
96+
return fmt.Errorf("creating stream builder: %w", err)
97+
}
98+
defer builder.Close()
99+
100+
builder.
101+
SetDirection(audio.Output).
102+
SetSampleRate(sampleRate).
103+
SetChannelCount(channels).
104+
SetFormat(audio.PcmI16)
105+
106+
stream, err := builder.Open()
107+
if err != nil {
108+
return fmt.Errorf("opening stream: %w", err)
109+
}
110+
defer func() {
111+
if closeErr := stream.Close(); closeErr != nil && _err == nil {
112+
_err = fmt.Errorf("closing stream: %w", closeErr)
113+
}
114+
}()
115+
116+
if err := stream.Start(); err != nil {
117+
return fmt.Errorf("starting stream: %w", err)
118+
}
119+
defer func() {
120+
if stopErr := stream.Stop(); stopErr != nil && _err == nil {
121+
_err = fmt.Errorf("stopping stream: %w", stopErr)
122+
}
123+
}()
124+
125+
f, err := os.Open(input)
126+
if err != nil {
127+
return fmt.Errorf("opening input file: %w", err)
128+
}
129+
defer f.Close()
130+
131+
framesPerBurst := stream.FramesPerBurst()
132+
// PCM16: 2 bytes per sample per channel
133+
bytesPerFrame := channels * 2
134+
buf := make([]byte, framesPerBurst*bytesPerFrame)
135+
timeout := 100 * time.Millisecond
136+
137+
var totalFrames int64
138+
for {
139+
n, err := f.Read(buf)
140+
switch {
141+
case err == nil:
142+
case err == io.EOF:
143+
fmt.Printf("played %d frames from %s\n", totalFrames, input)
144+
return nil
145+
default:
146+
return fmt.Errorf("reading from file: %w", err)
147+
}
148+
149+
framesToWrite := int32(n) / bytesPerFrame
150+
if framesToWrite == 0 {
151+
continue
152+
}
153+
154+
framesWritten, err := stream.Write(buf[:framesToWrite*bytesPerFrame], framesToWrite, timeout)
155+
if err != nil {
156+
return fmt.Errorf("writing to stream: %w", err)
157+
}
158+
totalFrames += int64(framesWritten)
159+
}
160+
},
161+
}
162+
163+
func init() {
164+
audioRecordCmd.Flags().String("output", "recording.pcm", "output file path")
165+
audioRecordCmd.Flags().Duration("duration", 5*time.Second, "recording duration")
166+
audioRecordCmd.Flags().Int32("sample-rate", 44100, "sample rate in Hz")
167+
audioRecordCmd.Flags().Int32("channels", 1, "number of audio channels")
168+
169+
audioPlayCmd.Flags().String("input", "recording.pcm", "input file path")
170+
audioPlayCmd.Flags().Int32("sample-rate", 44100, "sample rate in Hz")
171+
audioPlayCmd.Flags().Int32("channels", 1, "number of audio channels")
172+
173+
audioCmd.AddCommand(audioRecordCmd)
174+
audioCmd.AddCommand(audioPlayCmd)
175+
}

0 commit comments

Comments
 (0)