summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--rhimport/fetcher.go68
-rw-r--r--rhimport/wave_generator.go221
2 files changed, 262 insertions, 27 deletions
diff --git a/rhimport/fetcher.go b/rhimport/fetcher.go
index 2855ea0..e3280c4 100644
--- a/rhimport/fetcher.go
+++ b/rhimport/fetcher.go
@@ -556,36 +556,54 @@ func fetchFileDir(ctx *Context, res *Result, uri *url.URL, dir string, convert b
return
}
-func fetchFileFake(ctx *Context, res *Result, uri *url.URL) error {
- ctx.dbglog.Printf("Fake fetcher for '%s'", ctx.SourceUri)
+func fetchFileSilence(ctx *Context, res *Result, uri *url.URL) error {
+ ctx.dbglog.Printf("Silence fetcher for '%s'", ctx.SourceUri)
- duration, err := strconv.ParseUint(uri.Host, 10, 32)
+ d, err := strconv.ParseUint(uri.Host, 10, 32)
if err != nil {
res.ResponseCode = http.StatusBadRequest
res.ErrorString = "invalid duration (must be a positive integer)"
return nil
}
+ duration := time.Duration(d) * 100 * time.Millisecond
- for i := uint(0); i < uint(duration); i++ {
- if ctx.isCanceled() {
- ctx.stdlog.Printf("faking got canceled")
- res.ResponseCode = http.StatusNoContent
- res.ErrorString = "canceled"
- return nil
- }
-
- ctx.reportProgress(1, "faking", float64(i), float64(duration))
- time.Sleep(100 * time.Millisecond)
+ wav, err := NewPCMWavFile(uint32(ctx.conf.SampleRate), 16, uint16(ctx.Channels), duration)
+ if err != nil {
+ return err
}
- ctx.reportProgress(1, "faking", float64(duration), float64(duration))
+ fileSize := wav.GetFileSize()
+ wav.Generator = NewSilenceGenerator()
- ctx.SourceFile = "/nonexistend/fake.mp3"
- ctx.OrigFilename = ctx.SourceFile
- if ctx.SourceFilePolicy == Auto {
- ctx.DeleteSourceFile = false
- ctx.DeleteSourceDir = false
- }
- return nil
+ uri.Scheme = "attachment"
+ uri.Host = strconv.FormatUint(uint64(fileSize), 10)
+ uri.Path = "silence.wav"
+
+ go func() {
+ for {
+ chunk := AttachmentChunk{}
+ var data [32 * 1024]byte
+ n, err := wav.Read(data[:])
+ if n > 0 {
+ chunk.Data = data[:n]
+ if err == io.EOF {
+ err = nil
+ }
+ }
+ chunk.Error = err
+ if err == io.EOF {
+ return
+ }
+
+ // TODO: handle cancel
+ ctx.AttachmentChan <- chunk
+
+ if err != nil {
+ return
+ }
+ }
+ }()
+
+ return fetchFileAttachment(ctx, res, uri)
}
func writeAttachmentFile(ctx *Context, res *Result, sizeTotal uint64, conv FetchConverter) error {
@@ -633,11 +651,7 @@ func writeAttachmentFile(ctx *Context, res *Result, sizeTotal uint64, conv Fetch
}
func fetchFileAttachment(ctx *Context, res *Result, uri *url.URL) error {
- ctx.dbglog.Printf("Attachment fetcher for '%s'", ctx.SourceUri)
-
- if ctx.AttachmentChan == nil {
- return fmt.Errorf("attachement channel is nil")
- }
+ ctx.dbglog.Printf("Attachment fetcher for '%s'", uri.String())
sizeTotal, err := strconv.ParseUint(uri.Host, 10, 64)
if err != nil {
@@ -701,7 +715,7 @@ var (
fetchers = map[string]FetchFunc{
"local": fetchFileLocal,
"tmp": fetchFileTmp,
- "fake": fetchFileFake,
+ "silence": fetchFileSilence,
"attachment": fetchFileAttachment,
}
curlProtos = map[string]bool{
diff --git a/rhimport/wave_generator.go b/rhimport/wave_generator.go
new file mode 100644
index 0000000..f4c3261
--- /dev/null
+++ b/rhimport/wave_generator.go
@@ -0,0 +1,221 @@
+//
+// rhimportd
+//
+// The Radio Helsinki Rivendell Import Daemon
+//
+//
+// Copyright (C) 2015-2016 Christian Pointner <equinox@helsinki.at>
+//
+// This file is part of rhimportd.
+//
+// rhimportd is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// any later version.
+//
+// rhimportd is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with rhimportd. If not, see <http://www.gnu.org/licenses/>.
+//
+
+package rhimport
+
+import (
+ "bytes"
+ "encoding/binary"
+ "errors"
+ "io"
+ "math"
+ "time"
+)
+
+const (
+ RIFF_TAG = "RIFF"
+ WAVE_TAG = "WAVE"
+ FMT_TAG = "fmt "
+ FMT_ID_PCM = 0x0001
+ DATA_TAG = "data"
+)
+
+type wavHeader struct {
+ riffTag [4]uint8
+ riffLength uint32
+ waveTag [4]uint8
+
+ fmtTag [4]uint8
+ fmtLength uint32
+ fmtID uint16
+ nChannels uint16
+ sampleRate uint32
+ byteRate uint32
+ blockAlign uint16
+ sampleDepth uint16
+
+ dataTag [4]uint8
+ dataLength uint32
+}
+
+func (h *wavHeader) Bytes() []byte {
+ buf := new(bytes.Buffer)
+ binary.Write(buf, binary.LittleEndian, h)
+ return buf.Bytes()
+}
+
+type SampleGenerator interface {
+ Reset(samplePeriod float64)
+ GetSamples(nSamples uint32, channels uint16) []float64 // this needs to be normalized, aka -1 <= value <= 1
+}
+
+type WavFile struct {
+ header wavHeader
+ headerSize uint32
+ pcmSampleMax float64
+ pcmSampleBytes uint32
+ samplePeriod float64
+ Generator SampleGenerator
+ readOffset uint32
+}
+
+func (wav *WavFile) GetFileSize() (size uint32) {
+ return wav.headerSize + wav.header.dataLength
+}
+
+func (wav *WavFile) Read(p []byte) (n int, err error) {
+ n = 0
+ if wav.readOffset >= (wav.header.riffLength + 8) {
+ return n, io.EOF
+ }
+
+ if wav.readOffset < wav.headerSize {
+ n = copy(p, wav.header.Bytes()[wav.readOffset:])
+ wav.readOffset += uint32(n)
+ wav.Generator.Reset(wav.samplePeriod)
+ }
+ if n >= len(p) {
+ return
+ }
+ nsamples := uint32(len(p)-n) / uint32(wav.header.blockAlign)
+ data := wav.Generator.GetSamples(nsamples, wav.header.nChannels)
+
+ switch wav.header.fmtID {
+ case FMT_ID_PCM:
+ idx := 0
+ for _, normalized := range data {
+ scaled := wav.pcmSampleMax * normalized
+ sample := uint64(math.Trunc(math.Min(scaled, wav.pcmSampleMax)))
+ var b [8]byte
+ binary.LittleEndian.PutUint64(b[:], sample)
+ copy(p[n:n+int(wav.pcmSampleBytes)], b[:])
+ n += int(wav.pcmSampleBytes)
+ wav.readOffset += wav.pcmSampleBytes
+ if wav.readOffset >= (wav.header.riffLength + 8) {
+ return n, io.EOF
+ }
+ idx++
+ }
+ default:
+ return n, errors.New("unknown sample format ID")
+ }
+ return
+}
+
+func NewPCMWavFile(sampleRate uint32, sampleDepth uint16, channels uint16, length time.Duration) (wav *WavFile, err error) {
+ wav = &WavFile{}
+ wav.headerSize = 8 + 4 + 8 + 16 + 8
+ if length <= 0 {
+ return nil, errors.New("invalid length: must be > 0")
+ }
+ wav.pcmSampleMax = float64(uint64(1<<(sampleDepth-1)) - 1)
+ wav.pcmSampleBytes = ((uint32(sampleDepth) + 7) / 8)
+ wav.samplePeriod = 1.0 / float64(sampleRate)
+
+ frameSize32 := uint32(channels) * wav.pcmSampleBytes
+ if frameSize32 > math.MaxUint16 {
+ return nil, errors.New("frame size exceeds 16bit values (64kB)")
+ }
+ frameSize := uint16(frameSize32)
+
+ period_ns := float64(time.Second) / float64(sampleRate)
+ nFramesF64 := (float64(length) / period_ns)
+ if nFramesF64 < 1 {
+ nFramesF64 = 1
+ }
+ if nFramesF64 > math.MaxUint32 {
+ return nil, errors.New("number of frames exceeds limit (reduce length and/or sample-rate)")
+ }
+
+ nFrames := uint32(nFramesF64)
+ if nFrames > ((math.MaxUint32 - wav.headerSize) / uint32(frameSize)) {
+ return nil, errors.New("file length exceeds 32bit values (4GB)")
+ }
+ dataLen := nFrames * uint32(frameSize)
+
+ copy(wav.header.riffTag[:], RIFF_TAG)
+ wav.header.riffLength = wav.headerSize - 8 + dataLen
+ copy(wav.header.waveTag[:], WAVE_TAG)
+
+ copy(wav.header.fmtTag[:], FMT_TAG)
+ wav.header.fmtLength = 16
+ wav.header.fmtID = FMT_ID_PCM
+ wav.header.nChannels = channels
+ wav.header.sampleRate = sampleRate
+ wav.header.byteRate = sampleRate * uint32(frameSize)
+ wav.header.blockAlign = frameSize
+ wav.header.sampleDepth = sampleDepth
+
+ copy(wav.header.dataTag[:], DATA_TAG)
+ wav.header.dataLength = dataLen
+
+ return
+}
+
+type SilenceGenerator struct {
+}
+
+func NewSilenceGenerator() *SilenceGenerator {
+ return &SilenceGenerator{}
+}
+
+func (s *SilenceGenerator) Reset(samplePeriod float64) {
+ // nothing here
+}
+
+func (s *SilenceGenerator) GetSamples(nSamples uint32, nChannels uint16) (data []float64) {
+ data = make([]float64, int(nSamples)*int(nChannels))
+ return
+}
+
+type SinusGenerator struct {
+ amp float64
+ freq float64
+ sp float64
+ t float64
+}
+
+func NewSinusGenerator(ampDB, freq float64) (sin *SinusGenerator) {
+ sin = &SinusGenerator{}
+ sin.amp = math.Pow(10.0, (ampDB / 20.0))
+ sin.freq = freq
+ return
+}
+
+func (sin *SinusGenerator) Reset(samplePeriod float64) {
+ sin.sp = samplePeriod
+ sin.t = 0
+}
+
+func (sin *SinusGenerator) GetSamples(nSamples uint32, nChannels uint16) (data []float64) {
+ data = make([]float64, int(nSamples)*int(nChannels))
+ for i := 0; i < int(nSamples); i++ {
+ val := sin.amp * math.Sin(2*math.Pi*sin.freq*sin.t)
+ for j := 0; j < int(nChannels); j++ {
+ data[i*int(nChannels)+j] = val
+ }
+ sin.t += sin.sp
+ }
+ return
+}