diff --git a/.gitignore b/.gitignore index 3b735ec..ec6d44a 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,12 @@ # Dependency directories (remove the comment below to include it) # vendor/ +.idea/ +.vscode/ +target/ # Go workspace file go.work + +.DS_Store + diff --git a/generate_json.go b/go/generate_json.go similarity index 100% rename from generate_json.go rename to go/generate_json.go diff --git a/go.mod b/go/go.mod similarity index 100% rename from go.mod rename to go/go.mod diff --git a/go.sum b/go/go.sum similarity index 100% rename from go.sum rename to go/go.sum diff --git a/go/input.go b/go/input.go new file mode 100644 index 0000000..4ce6fb6 --- /dev/null +++ b/go/input.go @@ -0,0 +1,90 @@ +package main + +import ( + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" +) + +type Input interface { + Blink() tea.Msg + Blur() tea.Cmd + Focus() tea.Cmd + SetValue(string) + Value() string + Update(tea.Msg) (Input, tea.Cmd) + View() string +} + +type Path struct { + textinput textinput.Model + placeholder string +} + +func NewPath(placeholder string) *Path { + a := Path{} + model := textinput.New() + model.Placeholder = placeholder + model.Focus() + + a.textinput = model + return &a +} + +func (a *Path) Blink() tea.Msg { + return textinput.Blink() +} + +func (a *Path) Focus() tea.Cmd { + return a.textinput.Focus() +} + +func (a *Path) SetValue(s string) { + a.textinput.SetValue(s) +} + +func (a *Path) Value() string { + return a.textinput.Value() +} + +func (a *Path) Blur() tea.Cmd { + a.textinput.Blur() + return nil +} + +func (a *Path) Update(msg tea.Msg) (Input, tea.Cmd) { + var cmd tea.Cmd + a.textinput, cmd = a.textinput.Update(msg) + return a, cmd +} + +func (a *Path) View() string { + return a.textinput.View() +} + +//Digital representation of LocalFolderPath, WebFolderPath and LocalRootPath +type LocalFolderPath struct { + *Path +} + +func NewLocalFolderPathField() *LocalFolderPath { + placeholder := "example: /Users/username/samples/piano" + return &LocalFolderPath{NewPath(placeholder)} +} + +type WebFolderPath struct { + *Path +} + +func NewWebFolderPathField() *WebFolderPath { + placeholder := "example: https://raw.githubusercontent.com/username/samples/main/" + return &WebFolderPath{NewPath(placeholder)} +} + +type LocalRootPath struct { + *Path +} + +func NewLocalRootPathField() *LocalRootPath { + placeholder := "example: /Users/username/samples" + return &LocalRootPath{NewPath(placeholder)} +} diff --git a/main.go b/go/main.go similarity index 50% rename from main.go rename to go/main.go index 06b11fa..a5ba0a6 100644 --- a/main.go +++ b/go/main.go @@ -2,59 +2,85 @@ package main import ( "fmt" + "os/exec" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/muesli/reflow/indent" ) -type Path struct { +type pitchDetectorFinishedMsg struct{err error} + +func PitchDetectAndRenameFiles(path string) tea.Cmd { + c := exec.Command("pitch_detector", path) + return tea.ExecProcess(c, func(err error) tea.Msg { + return pitchDetectorFinishedMsg{err} +}) +} + +type PathInput struct { question string answer string input Input } type model struct { - styles *Styles - paths []Path - MainMenu int - RenameMenu bool - CreateJsonMenu bool - Quitting bool - width int - height int - index int - done string + styles *Styles + paths []PathInput + MainMenu int + PitchDetectAndRename bool + RenameMenu bool + CreateJsonMenu bool + Quitting bool + width int + height int + index int + doneMessage string } func main() { - questions := []Path{ + paths := []PathInput{ newLocalFolder("Local folder path"), - newRemoteFolder("Web folder path"), + newLocalRoot("Local root path"), + newRemoteFolder("Remote root folder path"), } styles := DefaultStyles() - initialModel := model{styles, questions, 0, false, false, false, 0, 0, 0, ""} + initialModel := model{ + styles: styles, + paths: paths, + MainMenu: 0, + PitchDetectAndRename: false, + RenameMenu: false, + CreateJsonMenu: false, + Quitting: false, + width: 0, + height: 0, + index: 0, + doneMessage: "", + } p := tea.NewProgram(initialModel) if _, err := p.Run(); err != nil { fmt.Println("could not start program:", err) } } -func newPath(q string) Path { - return Path{question: q} +func newPath(q string, placeholder string) PathInput { + path := PathInput{question: q, input: NewPath(placeholder)} + return path } -func newLocalFolder(q string) Path { - path := newPath(q) - model := NewLocalFolderPathField() - path.input = model - return path +func newLocalRoot(q string) PathInput { + placeholder := "example: /Users/username/samples" + return newPath(q, placeholder) } -func newRemoteFolder(q string) Path { - path := newPath(q) - model := NewWebFolderPathField() - path.input = model - return path +func newLocalFolder(q string) PathInput { + placeholder := "example: /Users/username/samples/piano" + return newPath(q, placeholder) +} + +func newRemoteFolder(q string) PathInput { + placeholder := "example: https://raw.githubusercontent.com/username/samples/main/" + return newPath(q, placeholder) } func (m model) Init() tea.Cmd { @@ -84,13 +110,14 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, tea.Quit } } - - if !m.RenameMenu && !m.CreateJsonMenu { + if !m.RenameMenu && !m.CreateJsonMenu && !m.PitchDetectAndRename { return updateChoices(msg, m) } else if m.RenameMenu { return updateRename(msg, m) } else if m.CreateJsonMenu { return updateCreateJson(msg, m) + } else if m.PitchDetectAndRename { + return updatePitchDetectAndRename(msg, m) } return m, nil @@ -102,13 +129,16 @@ func (m model) View() string { if m.Quitting { return "Quitting..." } - if !m.RenameMenu && !m.CreateJsonMenu { + if !m.RenameMenu && !m.CreateJsonMenu && !m.PitchDetectAndRename { s = choicesView(m) } else if m.RenameMenu { s = renameView(m) } else if m.CreateJsonMenu { s = createJsonView(m) + } else if m.PitchDetectAndRename { + s = PitchDetectAndRename(m) } + return indent.String("\n"+s+"\n\n", 2) } @@ -117,14 +147,15 @@ func choicesView(m model) string { c := m.MainMenu tpl := "Doughscraper\n\n" tpl += "%s\n\n" - tpl += subtle(m.done) + tpl += subtle(m.doneMessage) tpl += "\n" tpl += subtle("j/k, up/down: select") + dot + subtle("enter: choose") + dot + subtle("q, esc: quit") choices := fmt.Sprintf( - "%s\n%s", + "%s\n%s\n%s\n", checkbox("Rename pitched files", c == 0), checkbox("Generate JSON", c == 1), + checkbox("Detect pitch and rename", c == 2), ) return fmt.Sprintf(tpl, choices) @@ -137,13 +168,13 @@ func updateChoices(msg tea.Msg, m model) (tea.Model, tea.Cmd) { switch msg.String() { case "j", "down": m.MainMenu++ - if m.MainMenu > 1 { + if m.MainMenu > 2 { m.MainMenu = 0 } case "k", "up": m.MainMenu-- if m.MainMenu < 0 { - m.MainMenu = 1 + m.MainMenu = 2 } case "enter": switch m.MainMenu { @@ -151,15 +182,18 @@ func updateChoices(msg tea.Msg, m model) (tea.Model, tea.Cmd) { m.RenameMenu = true case 1: m.CreateJsonMenu = true + m.index = 1 + case 2: + m.PitchDetectAndRename = true } } } return m, nil } -// View for the "Create JSON" menu -func createJsonView(m model) string { - current := m.paths[m.index] +// View for the "Rename pitched files" menu +func renameView(m model) string { + current := m.paths[0] return lipgloss.Place( m.width, m.height, @@ -169,13 +203,41 @@ func createJsonView(m model) string { lipgloss.Left, current.question, m.styles.InputField.Render(current.input.View()), - lipgloss.JoinHorizontal(lipgloss.Bottom, subtle("Provide paths to the roots of your local and remote sample folders")), + lipgloss.JoinHorizontal(lipgloss.Bottom, subtle("Path to the folder with files to rename")), ), ) } -// View for the "Rename files" menu -func renameView(m model) string { +// Update for the "Rename pitched files" menu +func updateRename(msg tea.Msg, m model) (tea.Model, tea.Cmd) { + current := &m.paths[0] + var cmd tea.Cmd + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.width = msg.Width + m.height = msg.Height + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c", "q": + return m, tea.Quit + case "enter": + current.answer = current.input.Value() + err := RenameFiles(current.answer) + if err != nil { + fmt.Printf("Error: %s", err.Error()) + m.doneMessage = fmt.Sprintf("Error: %s", err.Error()) + } else { + m.doneMessage = fmt.Sprintf("Renamed files in %s", current.answer) + } + m.RenameMenu = false + return m, current.input.Blur() + } + } + current.input, cmd = current.input.Update(msg) + return m, cmd +} + +func PitchDetectAndRename(m model) string { current := m.paths[0] return lipgloss.Place( m.width, @@ -186,14 +248,13 @@ func renameView(m model) string { lipgloss.Left, current.question, m.styles.InputField.Render(current.input.View()), - lipgloss.JoinHorizontal(lipgloss.Bottom, subtle("Provide the path to the folder with files to rename.")), + lipgloss.JoinHorizontal(lipgloss.Bottom, subtle("Path to the folder with files to pitch detect and rename")), ), ) } -// Update for the "Create JSON" menu -func updateCreateJson(msg tea.Msg, m model) (tea.Model, tea.Cmd) { - current := &m.paths[m.index] +func updatePitchDetectAndRename(msg tea.Msg, m model) (tea.Model, tea.Cmd) { + current := &m.paths[0] var cmd tea.Cmd switch msg := msg.(type) { case tea.WindowSizeMsg: @@ -205,26 +266,43 @@ func updateCreateJson(msg tea.Msg, m model) (tea.Model, tea.Cmd) { return m, tea.Quit case "enter": current.answer = current.input.Value() - if m.index == len(m.paths)-1 { - err := GenerateJson(m.paths[0].answer, m.paths[1].answer) - if err != nil { - fmt.Printf("Error: %s", err.Error()) - m.done = fmt.Sprintf("Error: %s", err.Error()) - } - m.done = fmt.Sprintf("Generated JSON in %s", m.paths[0].answer) - m.CreateJsonMenu = false - } - m.Next() - return m, current.input.Blur + current.input.SetValue("Working...") + return m, PitchDetectAndRenameFiles(current.answer) } + case pitchDetectorFinishedMsg: + if msg.err != nil { + fmt.Printf("Error: %v", msg.err) + m.doneMessage = fmt.Sprintf("Error: %v", msg.err) + m.PitchDetectAndRename = false + } else { + m.doneMessage = fmt.Sprintf("Detected and renamed files in %s", current.answer) + m.PitchDetectAndRename = false + } } current.input, cmd = current.input.Update(msg) return m, cmd } -// Update for the "Rename files" menu -func updateRename(msg tea.Msg, m model) (tea.Model, tea.Cmd) { - current := &m.paths[0] +// View for the "Generate JSON" menu +func createJsonView(m model) string { + current := m.paths[m.index] + return lipgloss.Place( + m.width, + m.height, + lipgloss.Center, + lipgloss.Center, + lipgloss.JoinVertical( + lipgloss.Left, + current.question, + m.styles.InputField.Render(current.input.View()), + lipgloss.JoinHorizontal(lipgloss.Bottom, subtle("Provide paths to the roots of your local and remote sample folders")), + ), + ) +} + +// Update for the "Generate JSON" menu +func updateCreateJson(msg tea.Msg, m model) (tea.Model, tea.Cmd) { + current := &m.paths[m.index] var cmd tea.Cmd switch msg := msg.(type) { case tea.WindowSizeMsg: @@ -236,15 +314,17 @@ func updateRename(msg tea.Msg, m model) (tea.Model, tea.Cmd) { return m, tea.Quit case "enter": current.answer = current.input.Value() - err := RenameFiles(current.answer) - if err != nil { - fmt.Printf("Error: %s", err.Error()) - m.done = fmt.Sprintf("Error: %s", err.Error()) - } else { - m.done = fmt.Sprintf("Renamed files in %s", current.answer) + if m.index == len(m.paths)-1 { + err := GenerateJson(m.paths[1].answer, m.paths[2].answer) + if err != nil { + fmt.Printf("Error: %s", err.Error()) + m.doneMessage = fmt.Sprintf("Error: %s", err.Error()) + } + m.doneMessage = fmt.Sprintf("Generated JSON in %s", m.paths[1].answer) + m.CreateJsonMenu = false } - m.RenameMenu = false - return m, current.input.Blur + m.Next() + return m, current.input.Blur() } } current.input, cmd = current.input.Update(msg) diff --git a/rename_files.go b/go/rename_files.go similarity index 100% rename from rename_files.go rename to go/rename_files.go diff --git a/styles.go b/go/styles.go similarity index 100% rename from styles.go rename to go/styles.go diff --git a/input.go b/input.go deleted file mode 100644 index 564572d..0000000 --- a/input.go +++ /dev/null @@ -1,114 +0,0 @@ -package main - -import ( - "github.com/charmbracelet/bubbles/textinput" - tea "github.com/charmbracelet/bubbletea" -) - -type Input interface { - Blink() tea.Msg - Blur() tea.Msg - Focus() tea.Cmd - SetValue(string) - Value() string - Update(tea.Msg) (Input, tea.Cmd) - View() string -} - -type LocalFolderPath struct { - textinput textinput.Model -} - -func NewLocalFolderPathField() *LocalFolderPath { - a := LocalFolderPath{} - - model := textinput.New() - model.Placeholder = "example: /Users/username/samples" - model.Focus() - - a.textinput = model - return &a -} - -func (a *LocalFolderPath) Blink() tea.Msg { - return textinput.Blink() -} - -func (a *LocalFolderPath) Init() tea.Cmd { - return nil -} - -func (a *LocalFolderPath) Update(msg tea.Msg) (Input, tea.Cmd) { - var cmd tea.Cmd - a.textinput, cmd = a.textinput.Update(msg) - return a, cmd -} - -func (a *LocalFolderPath) View() string { - return a.textinput.View() -} - -func (a *LocalFolderPath) Focus() tea.Cmd { - return a.textinput.Focus() -} - -func (a *LocalFolderPath) SetValue(s string) { - a.textinput.SetValue(s) -} - -func (a *LocalFolderPath) Blur() tea.Msg { - return a.textinput.Blur -} - -func (a *LocalFolderPath) Value() string { - return a.textinput.Value() -} - -type WebFolderPath struct { - textinput textinput.Model -} - -func NewWebFolderPathField() *WebFolderPath { - a := WebFolderPath{} - - model := textinput.New() - model.Placeholder = "example: https://raw.githubusercontent.com/username/samples/main/" - model.Focus() - - a.textinput = model - return &a -} - -func (a *WebFolderPath) Blink() tea.Msg { - return textinput.Blink() -} - -func (a *WebFolderPath) Init() tea.Cmd { - return nil -} - -func (a *WebFolderPath) Update(msg tea.Msg) (Input, tea.Cmd) { - var cmd tea.Cmd - a.textinput, cmd = a.textinput.Update(msg) - return a, cmd -} - -func (a *WebFolderPath) View() string { - return a.textinput.View() -} - -func (a *WebFolderPath) Focus() tea.Cmd { - return a.textinput.Focus() -} - -func (a *WebFolderPath) SetValue(s string) { - a.textinput.SetValue(s) -} - -func (a *WebFolderPath) Blur() tea.Msg { - return a.textinput.Blur -} - -func (a *WebFolderPath) Value() string { - return a.textinput.Value() -} diff --git a/rust/pitch_detector/Cargo.lock b/rust/pitch_detector/Cargo.lock new file mode 100644 index 0000000..70d4698 --- /dev/null +++ b/rust/pitch_detector/Cargo.lock @@ -0,0 +1,270 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anyhow" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" + +[[package]] +name = "apodize" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fca387cdc0a1f9c7a7c26556d584aa2d07fc529843082e4861003cde4ab914ed" + +[[package]] +name = "approx" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0e60b75072ecd4168020818c0107f2857bb6c4e64252d8d3983f6263b40a5c3" +dependencies = [ + "num-traits", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "fitting" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25717d23402e00197e700694ffbc5fb005cdfe1edbbca1c0db544a2435b29719" +dependencies = [ + "approx", + "ndarray", + "thiserror", +] + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + +[[package]] +name = "hound" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "libc" +version = "0.2.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" + +[[package]] +name = "matrixmultiply" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916806ba0031cd542105d916a97c8572e1fa6dd79c9c51e7eb43a09ec2dd84c1" +dependencies = [ + "rawpointer", +] + +[[package]] +name = "ndarray" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac06db03ec2f46ee0ecdca1a1c34a99c0d188a0d83439b84bf0cb4b386e4ab09" +dependencies = [ + "approx", + "matrixmultiply", + "num-complex 0.2.4", + "num-integer", + "num-traits", + "rawpointer", +] + +[[package]] +name = "num-complex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b19411a9719e753aff12e5187b74d60d3dc449ec3f4dc21e3989c3f554bc95" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ba157ca0885411de85d6ca030ba7e2a83a28636056c7c699b07c8b6f7383214" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "pitch-detector" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c15e6d8f938411a08f18ecca4f309e7214df30b708c51cd07f9b6479b1cb07b" +dependencies = [ + "anyhow", + "apodize", + "fitting", + "itertools", + "num-traits", + "rustfft", +] + +[[package]] +name = "pitch_detector" +version = "0.1.0" +dependencies = [ + "anyhow", + "float-cmp", + "hound", + "libc", + "pitch-detector", +] + +[[package]] +name = "primal-check" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df7f93fd637f083201473dab4fee2db4c429d32e55e3299980ab3957ab916a0" +dependencies = [ + "num-integer", +] + +[[package]] +name = "proc-macro2" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + +[[package]] +name = "rustfft" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17d4f6cbdb180c9f4b2a26bbf01c4e647f1e1dea22fe8eb9db54198b32f9434" +dependencies = [ + "num-complex 0.4.4", + "num-integer", + "num-traits", + "primal-check", + "strength_reduce", + "transpose", + "version_check", +] + +[[package]] +name = "strength_reduce" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" + +[[package]] +name = "syn" +version = "2.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "transpose" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6522d49d03727ffb138ae4cbc1283d3774f0d10aa7f9bf52e6784c45daf9b23" +dependencies = [ + "num-integer", + "strength_reduce", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" diff --git a/rust/pitch_detector/Cargo.toml b/rust/pitch_detector/Cargo.toml new file mode 100644 index 0000000..6491b81 --- /dev/null +++ b/rust/pitch_detector/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "pitch_detector" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +pitch-detector = "0.3.1" +anyhow = "1.0.75" +float-cmp = "0.9.0" +hound = "3.5.1" +libc = "0.2.149" diff --git a/rust/pitch_detector/src/main.rs b/rust/pitch_detector/src/main.rs new file mode 100644 index 0000000..331c7db --- /dev/null +++ b/rust/pitch_detector/src/main.rs @@ -0,0 +1,87 @@ +use std::path::PathBuf; +use pitch_detector::{ + note::{detect_note_in_range, NoteDetectionResult}, + pitch::HannedFftDetector +}; +use anyhow::Result; +use hound; +use std::{env, fs}; + +const SAMPLE_RATE: f64 = 44100.0; +const MAX_FREQ: f64 = 1046.50; // C6 +const MIN_FREQ: f64 = 32.7; // C1 + +fn detect_note(sample: Vec) -> Result { + let mut detector = HannedFftDetector::default(); + let note = detect_note_in_range(&sample, &mut detector, SAMPLE_RATE, MIN_FREQ..MAX_FREQ) + .ok_or(anyhow::anyhow!("Did not get note"))?; + Ok(note) +} + +fn note(path: PathBuf) -> NoteDetectionResult { + let mut reader = hound::WavReader::open(path).unwrap(); + let mut f64_samples = Vec::new(); + for result in reader.samples::() { + let sample = result.unwrap(); + let normalized_sample = (sample << 8) as f64 / (i32::MAX as f64); + f64_samples.push(normalized_sample); + } + let pitch = detect_note(f64_samples).unwrap(); + pitch +} + +fn main() { + if let Some(dir_path) = env::args().nth(1) { + // Read the directory content. + for entry in fs::read_dir(dir_path).unwrap() { + let entry = entry.unwrap(); + let path = entry.path(); + + // Process only files (exclude directories). + if path.is_file() { + let old_name = path.file_name().ok_or("File name error").unwrap().to_string_lossy().to_string(); + + // Skip non-wave files. + if old_name.ends_with(".wav") || old_name.ends_with(".mp3") { + // Detect the pitch. + let pitch = note(path.clone()); + + // Generate a new name. + let mut new_name = String::new(); + match pitch.note_name.to_string().as_str() { + "A" => new_name = format!("a{}", pitch.octave.to_string()), + "A#" => new_name = format!("bb{}", pitch.octave.to_string()), + "B" => new_name = format!("b{}", pitch.octave.to_string()), + "C" => new_name = format!("c{}", pitch.octave.to_string()), + "C#" => new_name = format!("db{}", pitch.octave.to_string()), + "D" => new_name = format!("d{}", pitch.octave.to_string()), + "D#" => new_name = format!("eb{}", pitch.octave.to_string()), + "E" => new_name = format!("e{}", pitch.octave.to_string()), + "F" => new_name = format!("f{}", pitch.octave.to_string()), + "F#" => new_name = format!("gb{}", pitch.octave.to_string()), + "G" => new_name = format!("g{}", pitch.octave.to_string()), + "G#" => new_name = format!("ab{}", pitch.octave.to_string()), + _ => {} + } + let mut result = String::new(); + if old_name.ends_with(".wav") { + result = format!("{}-{}.wav", old_name.trim_end_matches(".wav"), new_name); + } else if old_name.ends_with(".mp3") { + result = format!("{}-{}.mp3", old_name.trim_end_matches(".mp3"), new_name); + } + + // Create a new path for the file. + let parent = path.parent().ok_or("Parent error").unwrap(); + let new_path = parent.join(result); + + // Rename the file. + fs::rename(path, new_path).unwrap(); + } + } + } + } else { + println!("Failed to get the directory path."); + } + +} +