Skip to content

Commit a031755

Browse files
committed
Added generators
1 parent 5926e60 commit a031755

File tree

9 files changed

+664
-14
lines changed

9 files changed

+664
-14
lines changed

frame.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ func (frame *frame) Time() time.Duration {
8282
if pts == ff.AV_NOPTS_VALUE {
8383
return -1
8484
}
85-
return secondsToDuration(float64(pts) * ff.AVUtil_q2d(frame.ctx.TimeBase()))
85+
return secondsToDuration(float64(pts) * ff.AVUtil_rational_q2d(frame.ctx.TimeBase()))
8686
}
8787

8888
// Return the number of planes for a specific PixelFormat

pkg/ffmpeg/frame.go

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
package media
2+
3+
import (
4+
"encoding/json"
5+
"image"
6+
"time"
7+
8+
// Packages
9+
media "github.com/mutablelogic/go-media"
10+
imagex "github.com/mutablelogic/go-media/pkg/image"
11+
ff "github.com/mutablelogic/go-media/sys/ffmpeg61"
12+
13+
// Namespace imports
14+
. "github.com/djthorpe/go-errors"
15+
)
16+
17+
////////////////////////////////////////////////////////////////////////////////
18+
// TYPES
19+
20+
type frame struct {
21+
ctx *ff.AVFrame
22+
}
23+
24+
var (
25+
yuvSubsampleRatio = map[ff.AVPixelFormat]image.YCbCrSubsampleRatio{
26+
ff.AV_PIX_FMT_YUV410P: image.YCbCrSubsampleRatio410,
27+
ff.AV_PIX_FMT_YUV411P: image.YCbCrSubsampleRatio410,
28+
ff.AV_PIX_FMT_YUV420P: image.YCbCrSubsampleRatio420,
29+
ff.AV_PIX_FMT_YUV422P: image.YCbCrSubsampleRatio422,
30+
ff.AV_PIX_FMT_YUV440P: image.YCbCrSubsampleRatio420,
31+
ff.AV_PIX_FMT_YUV444P: image.YCbCrSubsampleRatio444,
32+
}
33+
yuvaSubsampleRatio = map[ff.AVPixelFormat]image.YCbCrSubsampleRatio{
34+
ff.AV_PIX_FMT_YUVA420P: image.YCbCrSubsampleRatio420,
35+
ff.AV_PIX_FMT_YUVA422P: image.YCbCrSubsampleRatio422,
36+
ff.AV_PIX_FMT_YUVA444P: image.YCbCrSubsampleRatio444,
37+
}
38+
)
39+
40+
////////////////////////////////////////////////////////////////////////////////
41+
// LIFECYCLE
42+
43+
func NewFrame(ctx *ff.AVFrame) *frame {
44+
return &frame{ctx}
45+
}
46+
47+
////////////////////////////////////////////////////////////////////////////////
48+
// STRINGIFY
49+
50+
func (frame *frame) MarshalJSON() ([]byte, error) {
51+
return json.Marshal(frame.ctx)
52+
}
53+
54+
func (frame *frame) String() string {
55+
data, _ := json.MarshalIndent(frame, "", " ")
56+
return string(data)
57+
}
58+
59+
////////////////////////////////////////////////////////////////////////////////
60+
// PARAMETERS
61+
62+
// Return the media type (AUDIO, VIDEO)
63+
func (frame *frame) Type() media.MediaType {
64+
if frame.ctx.NumSamples() > 0 {
65+
return media.AUDIO
66+
}
67+
if frame.ctx.Width() != 0 && frame.ctx.Height() != 0 {
68+
return media.VIDEO
69+
}
70+
return media.NONE
71+
}
72+
73+
// Id is unused
74+
func (frame *frame) Id() int {
75+
return 0
76+
}
77+
78+
// Return the timestamp as a duration, or minus one if not set
79+
func (frame *frame) Time() time.Duration {
80+
pts := frame.ctx.Pts()
81+
if pts == ff.AV_NOPTS_VALUE {
82+
return -1
83+
}
84+
if frame.ctx.TimeBase().Den() == 0 {
85+
return -1
86+
}
87+
return secondsToDuration(float64(pts) * ff.AVUtil_rational_q2d(frame.ctx.TimeBase()))
88+
}
89+
90+
// Return the number of planes for a specific PixelFormat
91+
// or SampleFormat and ChannelLayout combination
92+
func (frame *frame) NumPlanes() int {
93+
return ff.AVUtil_frame_get_num_planes(frame.ctx)
94+
}
95+
96+
// Return the byte data for a plane
97+
func (frame *frame) Bytes(plane int) []byte {
98+
return frame.ctx.Bytes(plane)[:frame.ctx.Planesize(plane)]
99+
}
100+
101+
// Return the int16 data for a plane
102+
func (frame *frame) Int16(plane int) []int16 {
103+
sz := frame.ctx.Planesize(plane) >> 1
104+
return frame.ctx.Int16(plane)[:sz]
105+
}
106+
107+
////////////////////////////////////////////////////////////////////////////////
108+
// AUDIO PARAMETERS
109+
110+
// Return number of samples
111+
func (frame *frame) NumSamples() int {
112+
if frame.Type() != media.AUDIO {
113+
return 0
114+
}
115+
return frame.ctx.NumSamples()
116+
}
117+
118+
// Return channel layout
119+
func (frame *frame) ChannelLayout() string {
120+
if frame.Type() != media.AUDIO {
121+
return ""
122+
}
123+
ch := frame.ctx.ChannelLayout()
124+
if name, err := ff.AVUtil_channel_layout_describe(&ch); err != nil {
125+
return ""
126+
} else {
127+
return name
128+
}
129+
}
130+
131+
// Return the sample format
132+
func (frame *frame) SampleFormat() string {
133+
if frame.Type() != media.AUDIO {
134+
return ""
135+
}
136+
return ff.AVUtil_get_sample_fmt_name(frame.ctx.SampleFormat())
137+
}
138+
139+
// Return the sample rate (Hz)
140+
func (frame *frame) Samplerate() int {
141+
if frame.Type() != media.AUDIO {
142+
return 0
143+
}
144+
return frame.ctx.SampleRate()
145+
146+
}
147+
148+
////////////////////////////////////////////////////////////////////////////////
149+
// VIDEO PARAMETERS
150+
151+
// Convert a frame into an image
152+
func (frame *frame) Image() (image.Image, error) {
153+
if t := frame.Type(); t != media.VIDEO {
154+
return nil, ErrBadParameter.With("unsupported frame type", t)
155+
}
156+
pixel_format := frame.ctx.PixFmt()
157+
switch pixel_format {
158+
case ff.AV_PIX_FMT_GRAY8:
159+
return &image.Gray{
160+
Pix: frame.Bytes(0),
161+
Stride: frame.Stride(0),
162+
Rect: image.Rect(0, 0, frame.Width(), frame.Height()),
163+
}, nil
164+
case ff.AV_PIX_FMT_RGBA:
165+
return &image.RGBA{
166+
Pix: frame.Bytes(0),
167+
Stride: frame.Stride(0),
168+
Rect: image.Rect(0, 0, frame.Width(), frame.Height()),
169+
}, nil
170+
case ff.AV_PIX_FMT_RGB24:
171+
return &imagex.RGB24{
172+
Pix: frame.Bytes(0),
173+
Stride: frame.Stride(0),
174+
Rect: image.Rect(0, 0, frame.Width(), frame.Height()),
175+
}, nil
176+
default:
177+
if ratio, exists := yuvSubsampleRatio[pixel_format]; exists {
178+
return &image.YCbCr{
179+
Y: frame.Bytes(0),
180+
Cb: frame.Bytes(1),
181+
Cr: frame.Bytes(2),
182+
YStride: frame.Stride(0),
183+
CStride: frame.Stride(1),
184+
SubsampleRatio: ratio,
185+
Rect: image.Rect(0, 0, frame.Width(), frame.Height()),
186+
}, nil
187+
}
188+
if ratio, exists := yuvaSubsampleRatio[pixel_format]; exists {
189+
return &image.NYCbCrA{
190+
YCbCr: image.YCbCr{
191+
Y: frame.Bytes(0),
192+
Cb: frame.Bytes(1),
193+
Cr: frame.Bytes(2),
194+
YStride: frame.Stride(0),
195+
CStride: frame.Stride(1),
196+
SubsampleRatio: ratio,
197+
Rect: image.Rect(0, 0, frame.Width(), frame.Height()),
198+
},
199+
A: frame.Bytes(3),
200+
AStride: frame.Stride(3),
201+
}, nil
202+
}
203+
}
204+
return nil, ErrNotImplemented.With("unsupported pixel format", frame.ctx.PixFmt())
205+
}
206+
207+
// Return the number of bytes in a single row of the video frame
208+
func (frame *frame) Stride(plane int) int {
209+
if frame.Type() == media.VIDEO {
210+
return frame.ctx.Linesize(plane)
211+
} else {
212+
return 0
213+
}
214+
}
215+
216+
// Return the width of the video frame
217+
func (frame *frame) Width() int {
218+
if frame.Type() != media.VIDEO {
219+
return 0
220+
}
221+
return frame.ctx.Width()
222+
}
223+
224+
// Return the height of the video frame
225+
func (frame *frame) Height() int {
226+
if frame.Type() != media.VIDEO {
227+
return 0
228+
}
229+
return frame.ctx.Height()
230+
}
231+
232+
// Return the pixel format
233+
func (frame *frame) PixelFormat() string {
234+
if frame.Type() != media.VIDEO {
235+
return ""
236+
}
237+
return ff.AVUtil_get_pix_fmt_name(frame.ctx.PixFmt())
238+
}
239+
240+
////////////////////////////////////////////////////////////////////////////////
241+
// PRIVATE METHODS
242+
243+
func secondsToDuration(seconds float64) time.Duration {
244+
return time.Duration(seconds * float64(time.Second))
245+
}

pkg/generator/generator.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package generator
2+
3+
import (
4+
"io"
5+
6+
// Packages
7+
media "github.com/mutablelogic/go-media"
8+
)
9+
10+
////////////////////////////////////////////////////////////////////////////
11+
// INTERFACE
12+
13+
// Generator is an interface for generating frames of audio or video
14+
type Generator interface {
15+
io.Closer
16+
17+
// Return a generated frame
18+
Frame() media.Frame
19+
}

pkg/generator/sine.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package generator
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"math"
7+
"time"
8+
9+
// Packages
10+
media "github.com/mutablelogic/go-media"
11+
ffmpeg "github.com/mutablelogic/go-media/pkg/ffmpeg"
12+
ff "github.com/mutablelogic/go-media/sys/ffmpeg61"
13+
)
14+
15+
////////////////////////////////////////////////////////////////////////////
16+
// LIFECYCLE
17+
18+
type sine struct {
19+
frame *ff.AVFrame
20+
frequency float64 // in Hz
21+
volume float64 // in decibels
22+
}
23+
24+
var _ Generator = (*sine)(nil)
25+
26+
////////////////////////////////////////////////////////////////////////////
27+
// GLOBALS
28+
29+
const (
30+
frameDuration = 20 * time.Millisecond // Each frame is 20ms of audio
31+
)
32+
33+
////////////////////////////////////////////////////////////////////////////
34+
// LIFECYCLE
35+
36+
// Sine wave generator - mono float32
37+
func NewSine(freq float64, volume float64, samplerate int) (*sine, error) {
38+
sine := new(sine)
39+
40+
// Check parameters
41+
if freq <= 0 {
42+
return nil, errors.New("invalid frequency")
43+
}
44+
if volume <= -100 {
45+
return nil, errors.New("invalid volume")
46+
}
47+
if samplerate <= 0 {
48+
return nil, errors.New("invalid samplerate")
49+
}
50+
51+
// Create a frame
52+
frame := ff.AVUtil_frame_alloc()
53+
if frame == nil {
54+
return nil, errors.New("failed to allocate frame")
55+
}
56+
57+
// Set frame parameters
58+
numSamples := int(float64(samplerate) * frameDuration.Seconds())
59+
60+
frame.SetSampleFormat(ff.AV_SAMPLE_FMT_FLT) // float32
61+
if err := frame.SetChannelLayout(ff.AV_CHANNEL_LAYOUT_MONO); err != nil {
62+
return nil, err
63+
}
64+
frame.SetSampleRate(samplerate)
65+
frame.SetNumSamples(numSamples)
66+
frame.SetTimeBase(ff.AVUtil_rational(1, samplerate))
67+
frame.SetPts(ff.AV_NOPTS_VALUE)
68+
69+
// Allocate buffer
70+
if err := ff.AVUtil_frame_get_buffer(frame, false); err != nil {
71+
return nil, err
72+
} else {
73+
sine.frame = frame
74+
sine.frequency = freq
75+
sine.volume = volume
76+
}
77+
78+
// Return success
79+
return sine, nil
80+
}
81+
82+
// Free resources
83+
func (s *sine) Close() error {
84+
ff.AVUtil_frame_free(s.frame)
85+
s.frame = nil
86+
return nil
87+
}
88+
89+
////////////////////////////////////////////////////////////////////////////
90+
// STRINGIFY
91+
92+
func (s *sine) String() string {
93+
data, _ := json.MarshalIndent(s.frame, "", " ")
94+
return string(data)
95+
}
96+
97+
////////////////////////////////////////////////////////////////////////////
98+
// PUBLIC METHODS
99+
100+
func (s *sine) Frame() media.Frame {
101+
// Set the Pts
102+
if s.frame.Pts() == ff.AV_NOPTS_VALUE {
103+
s.frame.SetPts(0)
104+
} else {
105+
s.frame.SetPts(s.frame.Pts() + int64(s.frame.NumSamples()))
106+
}
107+
108+
// Calculate current phase and volume
109+
t := ff.AVUtil_rational_q2d(s.frame.TimeBase()) * float64(s.frame.Pts())
110+
volume := math.Pow(10, s.volume/20.0)
111+
data := s.frame.Float32(0)
112+
113+
// Generate sine wave
114+
for n := 0; n < s.frame.NumSamples(); n++ {
115+
sampleTime := t + float64(n)/float64(s.frame.SampleRate())
116+
data[n] = float32(math.Sin(2.0*math.Pi*s.frequency*sampleTime) * volume)
117+
}
118+
119+
// Return the frame
120+
return ffmpeg.NewFrame(s.frame)
121+
}

0 commit comments

Comments
 (0)