Feat: Working PoC

This commit is contained in:
kayos@tcp.direct 2022-03-12 00:43:05 -08:00
parent d8b4e628a4
commit fa0b2f104e
Signed by: kayos
GPG Key ID: 4B841471B4BEE979
5 changed files with 205 additions and 62 deletions

@ -4,6 +4,8 @@ import (
"os"
"runtime"
"strings"
"sync/atomic"
"time"
"github.com/rs/zerolog"
"kr.dev/walk"
@ -44,34 +46,38 @@ func main() {
slog.Debug().Msg("skiping directory entirely")
cripwalk.SkipDir()
}
slog.Trace().Msg("directory")
// slog.Trace().Msg("directory")
case cripwalk.Path() == os.Args[1]:
slog.Debug().Msg("skipping self-parent directory entirely")
cripwalk.SkipParent()
default:
sample, err := collect.Process(cripwalk.Entry(), util.APath(cripwalk.Path(), config.Relative))
if err != nil {
log.Warn().Err(err).Msgf("failed to process %s", cripwalk.Entry().Name())
slog.Warn().Err(err).Msgf("failed to process")
continue
}
if sample == nil {
log.Trace().Msgf("skipping unknown file %s", cripwalk.Entry().Name())
slog.Trace().Msgf("skipping unknown file")
continue
}
collect.Library.IngestTempo(sample)
}
}
if zerolog.GlobalLevel() == zerolog.TraceLevel {
collect.Library.TempoStats()
for !atomic.CompareAndSwapInt32(&collect.Backlog, 0, -1) {
time.Sleep(1 * time.Second)
print(".")
}
if config.StatsOnly {
return
collect.Library.TempoStats()
collect.Library.KeyStats()
collect.Library.DrumStats()
}
err := collect.Library.SymlinkTempos()
if err != nil {
log.Fatal().Err(err).Msg("returned from symlinkTempos")
}
var errs []error
errs = append(errs, collect.Library.SymlinkTempos())
errs = append(errs, collect.Library.SymlinkKeys())
errs = append(errs, collect.Library.SymlinkDrums())
log.Info().Errs("errs", errs).Msg("fin.")
}

@ -97,22 +97,62 @@ func (c *Collection) TempoStats() {
}
}
// DrumStats outputs the amount of samples of each known drum type.
func (c *Collection) DrumStats() {
c.mu.RLock()
defer c.mu.RUnlock()
for t, ss := range c.Drums {
if len(ss) > 1 {
log.Printf("%s: %d", drumToDirMap[t], len(ss))
}
}
}
// KeyStats outputs the amount of samples with each known key.
func (c *Collection) KeyStats() {
c.mu.RLock()
defer c.mu.RUnlock()
for t, ss := range c.Keys {
if len(ss) > 1 {
log.Printf("%s: %d", t.Root.String(t.AdjSymbol), len(ss))
}
}
}
func link(sample *Sample, kp string) {
slog := log.With().Str("caller", sample.Path).Logger()
finalPath := kp + sample.Name
slog.Trace().Msg(finalPath)
err := freshLink(finalPath)
if err != nil {
slog.Warn().Err(err).Msg("old symlink delete failure")
}
if _, err = os.Stat(sample.Path); err != nil {
slog.Warn().Err(err).Msg("can't stat original file")
}
if config.Simulate {
log.Printf("would have linked %s -> %s", sample.Path, finalPath)
return
}
err = os.Symlink(sample.Path, finalPath)
if err != nil && !os.IsNotExist(err) {
slog.Error().Err(err).Msg("failed to create symlink")
}
}
func (c *Collection) SymlinkTempos() (err error) {
log.Trace().Msg("SymlinkTempos start")
defer log.Trace().Err(err).Msg("SymlinkTempos finish")
c.mu.RLock()
defer c.mu.RUnlock()
if len(c.Tempos) < 1 {
return errors.New("no tempos recorded")
return errors.New("no known tempos")
}
dst := util.APath(config.Destination+"Tempo", config.Relative)
err = os.MkdirAll(dst, os.ModePerm)
if err != nil && !os.IsNotExist(err) {
return
}
for t, ss := range c.Tempos {
tempopath := dst + "/" + strconv.Itoa(t) + "/"
err = os.MkdirAll(tempopath, os.ModePerm)
@ -120,21 +160,61 @@ func (c *Collection) SymlinkTempos() (err error) {
return
}
for _, s := range ss {
go func(sample *Sample) {
finalPath := tempopath + sample.Name
log.Trace().Str("caller", sample.Path).Msg(finalPath)
err = freshLink(finalPath)
if err != nil {
return
}
if _, err = os.Stat(sample.Path); err != nil {
return
}
err = os.Symlink(sample.Path, finalPath)
if err != nil && !os.IsNotExist(err) {
log.Error().Err(err).Msg("failed to create symlink")
}
}(s)
go link(s, tempopath)
}
}
return nil
}
func (c *Collection) SymlinkKeys() (err error) {
log.Trace().Msg("SymlinkKeys start")
defer log.Trace().Err(err).Msg("SymlinkKeys finish")
c.mu.RLock()
defer c.mu.RUnlock()
if len(c.Keys) < 1 {
return errors.New("no known keys")
}
dst := util.APath(config.Destination+"Key", config.Relative)
err = os.MkdirAll(dst, os.ModePerm)
if err != nil && !os.IsNotExist(err) {
return
}
for t, ss := range c.Keys {
keypath := dst + "/" + t.Root.String(t.AdjSymbol) + "/"
err = os.MkdirAll(keypath, os.ModePerm)
if err != nil && !os.IsExist(err) {
return
}
for _, s := range ss {
go link(s, keypath)
}
}
return nil
}
func (c *Collection) SymlinkDrums() (err error) {
log.Trace().Msg("SymlinkDrums start")
defer log.Trace().Err(err).Msg("SymlinkDrums finish")
c.mu.RLock()
defer c.mu.RUnlock()
if len(c.Drums) < 1 {
return errors.New("no known drums")
}
dst := util.APath(config.Destination+"Drums", config.Relative)
err = os.MkdirAll(dst, os.ModePerm)
if err != nil && !os.IsNotExist(err) {
return
}
for t, ss := range c.Drums {
drumpath := dst + "/" + drumToDirMap[t] + "/"
err = os.MkdirAll(drumpath, os.ModePerm)
if err != nil && !os.IsExist(err) {
return
}
for _, s := range ss {
go link(s, drumpath)
}
}
return nil

@ -1,7 +1,14 @@
package collect
import "sync/atomic"
var Backlog int32
// IngestKey creates a map of tempo to sample.
func (c *Collection) IngestKey(sample *Sample) {
log.Debug().Str("caller", sample.Name).Msgf("Key: %s", sample.Key.Root.String(sample.Key.AdjSymbol))
atomic.AddInt32(&Backlog, 1)
defer atomic.AddInt32(&Backlog, -1)
c.mu.Lock()
defer c.mu.Unlock()
c.Keys[sample.Key] = append(c.Keys[sample.Key], sample)
@ -9,6 +16,12 @@ func (c *Collection) IngestKey(sample *Sample) {
// IngestTempo creates a map of tempo to sample.
func (c *Collection) IngestTempo(sample *Sample) {
if sample.Tempo == 0 || sample.Tempo < 50 || sample.Tempo > 250 {
return
}
log.Debug().Str("caller", sample.Name).Msgf("Tempo: %d", sample.Tempo)
atomic.AddInt32(&Backlog, 1)
defer atomic.AddInt32(&Backlog, -1)
c.mu.Lock()
defer c.mu.Unlock()
c.Tempos[sample.Tempo] = append(c.Tempos[sample.Tempo], sample)
@ -16,6 +29,9 @@ func (c *Collection) IngestTempo(sample *Sample) {
// IngestMelodicLoop appends to a list of [pointers to] melodic loop samples.
func (c *Collection) IngestMelodicLoop(sample *Sample) {
log.Debug().Str("caller", sample.Name).Msg("Melodic Loop")
atomic.AddInt32(&Backlog, 1)
defer atomic.AddInt32(&Backlog, -1)
c.mu.Lock()
defer c.mu.Unlock()
c.MelodicLoops = append(c.MelodicLoops, sample)
@ -23,6 +39,9 @@ func (c *Collection) IngestMelodicLoop(sample *Sample) {
// IngestMIDI appends to a list of [pointers to] MIDI/SMF files.
func (c *Collection) IngestMIDI(sample *Sample) {
log.Debug().Str("caller", sample.Name).Msg("MIDI")
atomic.AddInt32(&Backlog, 1)
defer atomic.AddInt32(&Backlog, -1)
c.mu.Lock()
defer c.mu.Unlock()
c.Midis = append(c.Midis, sample)
@ -30,6 +49,9 @@ func (c *Collection) IngestMIDI(sample *Sample) {
// IngestDrum creates a map of different drum types to samples.
func (c *Collection) IngestDrum(sample *Sample, drumType DrumType) {
log.Debug().Str("caller", sample.Name).Msgf("Drum: %s", drumToDirMap[drumType])
atomic.AddInt32(&Backlog, 1)
defer atomic.AddInt32(&Backlog, -1)
c.mu.Lock()
defer c.mu.Unlock()
c.Drums[drumType] = append(c.Drums[drumType], sample)

@ -6,10 +6,12 @@ import (
"os"
"strconv"
"strings"
"sync/atomic"
"github.com/go-audio/wav"
"gopkg.in/music-theory.v0/key"
"gopkg.in/music-theory.v0/note"
"git.tcp.direct/kayos/keepr/internal/config"
)
func freshLink(path string) error {
@ -21,7 +23,11 @@ func freshLink(path string) error {
return nil
}
func checkbpm(piece string) (bpm int) {
func guessBPM(piece string) (bpm int) {
// TODO: don't trust this lol?
if num, numerr := strconv.Atoi(piece); numerr != nil {
return num
}
frg := strings.Split(piece, "bpm")[0]
m := strings.Split(frg, "")
var start = 0
@ -76,31 +82,45 @@ var drumDirMap = map[string]DrumType{
"open_hihats": HatOpen, "808s": EightOhEight, "808": EightOhEight, "toms": Tom,
}
var drumToDirMap = map[DrumType]string{
Snare: "Snares", Kick: "Kicks", HiHat: "HiHats", HatClosed: "HiHat/Closed",
HatOpen: "HiHat/Open", EightOhEight: "808", Tom: "Toms", Percussion: "Other",
}
func (s *Sample) ParseFilename() {
for _, piece := range guessSeperator(s.Name) {
piece = strings.ToLower(piece)
atomic.AddInt32(&Backlog, 1)
defer atomic.AddInt32(&Backlog, -1)
drumtype, isdrum := drumDirMap[s.getParentDir()]
switch {
case s.getParentDir() == "melodic_loops":
if !s.IsType(Loop) {
s.Type = append(s.Type, Loop)
go Library.IngestMelodicLoop(s)
}
case isdrum:
go Library.IngestDrum(s, drumtype)
}
for _, opiece := range guessSeperator(s.Name) {
piece := strings.ToLower(opiece)
if num, numerr := strconv.Atoi(piece); numerr == nil {
if num > 50 && num != 808 {
s.Tempo = num
}
}
if strings.Contains(piece, "bpm") {
s.Tempo = checkbpm(piece)
if s.Tempo != 0 {
s.Type = append(s.Type, Loop)
}
s.Tempo = guessBPM(piece)
}
drumtype, isdrum := drumDirMap[s.getParentDir()]
switch {
case s.getParentDir() == "melodic_loops":
if !s.IsType(Loop) {
s.Type = append(s.Type, Loop)
go Library.IngestMelodicLoop(s)
}
if isdrum {
go Library.IngestDrum(s, drumtype)
}
if s.Tempo != 0 {
go Library.IngestTempo(s)
}
spl := strings.Split(piece, "")
spl := strings.Split(opiece, "")
if len(spl) < 1 {
continue
}
// if our fragment starts with a known root note, then try to parse the fragment, else dip-set.
switch spl[0] {
case "C", "D", "E", "F", "G", "A", "B":
@ -109,11 +129,8 @@ func (s *Sample) ParseFilename() {
continue
}
k := key.Of(piece)
if k.Root != note.Nil {
s.Key = k
Library.IngestKey(s)
}
s.Key = key.Of(opiece)
go Library.IngestKey(s)
}
}
@ -133,6 +150,10 @@ func readWAV(s *Sample) error {
if decoder.Err() != nil {
return decoder.Err()
}
if meta := decoder.Metadata; meta == nil {
return nil
}
s.Metadata = decoder.Metadata
log.Trace().Msg(fmt.Sprintf("metadata: %v", s.Metadata))
@ -142,6 +163,7 @@ func readWAV(s *Sample) error {
}
func Process(entry fs.DirEntry, dir string) (s *Sample, err error) {
log.Trace().Str("caller", entry.Name()).Msg("Processing")
var finfo os.FileInfo
finfo, err = entry.Info()
if err != nil {
@ -159,13 +181,18 @@ func Process(entry fs.DirEntry, dir string) (s *Sample, err error) {
switch ext {
case "midi", "mid":
s.Type = append(s.Type, MIDI)
if !config.NoMIDI {
s.Type = append(s.Type, MIDI)
go Library.IngestMIDI(s)
}
case "wav":
err = readWAV(s)
if !config.SkipWavDecode {
err = readWAV(s)
}
if err != nil {
return nil, err
}
s.ParseFilename()
go s.ParseFilename()
default:
return nil, nil
}

@ -16,9 +16,11 @@ var (
// Destination is the base path for our symlink library.
Destination = defDestination
// Relative will determine if we use relative pathing for symlinks.
Relative = false
StatsOnly = false
Relative = false
Simulate = false
StatsOnly = false
NoMIDI = false
SkipWavDecode = false
)
// GetLogger retrieves a pointer to our zerolog instance.
@ -55,6 +57,12 @@ func init() {
Relative = true
case "--stats", "-s":
StatsOnly = true
case "--no-op", "-n":
Simulate = true
case "--no-midi", "-m":
NoMIDI = true
case "--fast", "-f":
SkipWavDecode = true
default:
Target = strings.Trim(arg, "/")
println("search target detected: " + Target)