diff options
Diffstat (limited to 'rhimport')
-rw-r--r-- | rhimport/fetcher.go | 68 | ||||
-rw-r--r-- | rhimport/wave_generator.go | 221 |
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 +} |