#haskell
#haskell
Вопрос:
Итак, в следующем коде я генерирую wav-файл из нот и составленных аккордов. У меня это работает для одиночных нот и аккордов из двух нот, но для комбинаций из более чем 2 нот я сталкиваюсь с проблемами, потому что я не нормирую частоты. Я знаю, что мне нужно сделать (разделить частоты в каждом кадре на количество составляющих его нот), но не обязательно, как сделать это элегантно (или вообще каким-либо образом). Что должно произойти, так это то, что мне нужно каким-то образом получить длину списка, возвращаемого от notes''
до buildChord
, а затем решить, как сопоставить деление на это число во входных данных с buildChord
.
Я действительно в растерянности, поэтому буду признателен за любой ввод.
import Data.WAVE
import Control.Applicative
import Data.Char (isDigit)
import Data.Function (on)
import Data.Int (Int32)
import Data.List (transpose, groupBy)
import Data.List.Split (splitOn, split, oneOf)
import System.IO (hGetContents, Handle, openFile, IOMode(..))
a4 :: Double
a4 = 440.0
frameRate :: Int
frameRate = 32000
noteLength :: Double
noteLength = 1
volume :: Int32
volume = maxBound `div` 2
buildChord :: [[Double]] -> WAVESamples
buildChord freqs = map ((:[]) . round . sum) $ transpose freqs
generateSoundWave :: Int -- | Samples Per Second
-> Double -- | Length of Sound in Seconds
-> Int32 -- | Volume
-> Double -- | Frequency
-> [Double]
generateSoundWave sPS len vol freq =
take (round $ len * fromIntegral sPS) $
map ((* fromIntegral vol) . sin)
[0.0, (freq * 2 * pi / fromIntegral sPS)..]
generateSoundWaves :: Int -- | Samples Per Second
-> Double -- | Length of Sound in Seconds
-> Int32 -- | Volume
-> [Double] -- | Frequency
-> [[Double]]
generateSoundWaves sPS len vol =
map (generateSoundWave sPS len vol)
noteToSine :: String -> WAVESamples
noteToSine chord =
buildChord $ generateSoundWaves frameRate noteLength volume freqs
where freqs = getFreqs $ notes chord
notes'' :: String -> [String]
notes'' = splitOn "/"
notes' :: [String] -> [[String]]
notes' = map (split (oneOf "1234567890"))
notes :: String -> [(String, Int)]
notes chord = concatMap pair $ notes' $ notes'' chord
where pair (x:y:ys) = (x, read y :: Int) : pair ys
pair _ = []
notesToSines :: String -> WAVESamples
notesToSines = concatMap noteToSine . splitOn " "
getFreq :: (String, Int) -> Double
getFreq (note, octave) =
if octave >= -1 amp;amp; octave < 10 amp;amp; n /= 12.0
then a4 * 2 ** ((o - 4.0) ((n - 9.0) / 12.0))
else undefined
where o = fromIntegral octave :: Double
n = case note of
"B#" -> 0.0
"C" -> 0.0
"C#" -> 1.0
"Db" -> 1.0
"D" -> 2.0
"D#" -> 3.0
"Eb" -> 3.0
"E" -> 4.0
"Fb" -> 4.0
"E#" -> 5.0
"F" -> 5.0
"F#" -> 6.0
"Gb" -> 6.0
"G" -> 7.0
"G#" -> 8.0
"Ab" -> 8.0
"A" -> 9.0
"A#" -> 10.0
"Bb" -> 10.0
"B" -> 11.0
"Cb" -> 11.0
_ -> 12.0
getFreqs :: [(String, Int)] -> [Double]
getFreqs = map getFreq
header :: WAVEHeader
header = WAVEHeader 1 frameRate 32 Nothing
getFileName :: IO FilePath
getFileName = putStr "Enter the name of the file: " >> getLine
getChordsAndOctaves :: IO String
getChordsAndOctaves = getFileName >>= n ->
openFile n ReadMode >>=
hGetContents
main :: IO ()
main = getChordsAndOctaves >>= co ->
putWAVEFile "out.wav" (WAVE header $ notesToSines co)
Ответ №1:
Ключевая проблема была связана с функцией:
buildChord :: [[Double]] -> WAVESamples
buildChord freqs = map ((:[]) . round . sum) $ transpose freqs
Результатом transpose freqs
был список громкости звука на определенный момент времени для каждой воспроизводимой ноты (например [45.2, 20, -10]
). Функция (:[] . round . sum)
сначала складывает их вместе (например, 55.2
), округляет (например, до 55
) и помещает в список (например, [55]
). map (:[] . round . sum)
только что сделал это для всех случаев time.
Проблема в том, что если у вас одновременно воспроизводится много нот, в сумме получается слишком громкая нота. Что было бы лучше, так это взять среднее значение нот, а не сумму. Это означает, что одновременное воспроизведение 10 нот не будет слишком громким. Удивительно, но в prelude нет функции average. Таким образом, мы можем либо написать нашу собственную функцию average, либо просто встроить ее в функцию, переданную map. Я сделал последнее, поскольку это было меньше кода:
buildChord :: [[Double]] -> WAVESamples
buildChord freqs = map (chord -> [round $ sum chord / genericLength chord]) $ transpose freqs
Из ваших вопросов я предполагаю, что вы пишете программу для создания музыки как способ изучения haskell. У меня есть несколько идей, которые могут упростить отладку вашего кода и сделать его более «похожим на haskell».
Код в haskell часто записывается как последовательность преобразований от ввода к выводу. Эта функция buildChord является хорошим примером — сначала входные данные были транспонированы, затем сопоставлены с помощью функции, которая объединила несколько амплитуд звука. Однако вы также могли бы структурировать всю свою программу в этом стиле.
Цель программы, по-видимому, такова: «прочитать ноты из файла в некотором формате, затем создать wav-файл из прочитанных нот». Способ, которым я бы решил эту проблему, состоял бы в том, чтобы сначала разбить это на разные чистые преобразования (т. Е. Без использования ввода или вывода) и выполнить чтение и запись в качестве последнего шага.
Я бы сначала начал с написания преобразования звуковой волны в ВОЛНОВУЮ. Я бы использовал тип:
data Sound = Sound { soundFreqs :: [Double]
, soundVolume :: Double
, soundLength :: Double
}
Затем напишите функцию:
soundsToWAVE :: Int -> [Sound] -> WAVE
soundsToWAVE samplesPerSec sounds = undefined -- TODO
Тогда я мог бы написать функции writeSoundsToWavFile
и testPlaySounds
:
writeSoundsToWavFile :: String -> Int -> [Sound] -> IO ()
writeSoundsToWavFile fileN samplesPerSec sounds = putWAVEFile $ soundsToWAVE fileN samplesPerSec sounds
testPlaySounds :: [Sound] -> IO ()
testPlaySounds sounds = do
writeSoundsToWavFile "test.wav" 32000 sounds
system("afplay test.wav") -- use aplay on linux, don't know for windows
return ()
Как только это будет сделано, весь код WAVE будет выполнен — остальной части кода не нужно его касаться. Возможно, было бы неплохо поместить это в свой собственный модуль.
После этого я бы написал преобразование между музыкальными нотами и Звуками. Я бы использовал следующие типы нот:
data Note = A | B | C | D | E | F | G
data NoteAugment = None | Sharp | Flat
data MusicNote = MusicNote { note :: Note, noteAugment :: NoteAugment, noteOctave :: Int }
data Chord = Chord { notes :: [MusicNote], chordVolume :: Double }
Затем напишите функцию:
chordToSound :: Chord -> Sound
chordToSound = undefined -- TODO
Затем вы могли бы легко написать функцию musicNotesToWAVFile:
chordsToWAVFile fileName samplesPerSec notes = writeSoundsToWavFile 32000 fileName samplesPerSec (map chordToSound notes)
(функция testPlayChords может быть выполнена таким же образом). Вы также могли бы поместить это в новый модуль.
Наконец, я бы написал строку ноты преобразования -> [Аккорд]. Для этого просто нужна функция:
parseNoteFileText :: String -> [Chord]
parseNoteFileText noteText = undefined
Затем можно было бы подключить окончательную программу:
main = do
putStrLn "Enter the name of the file: "
fileN <- getLine
noteText <- readFile fileN
chordsToWAVFile (parseNoteFileText noteText)