Skip to content

Commit

Permalink
Add tuning frequency estimation prototype #1
Browse files Browse the repository at this point in the history
  • Loading branch information
jurihock committed Mar 29, 2024
1 parent f4ef254 commit 2dfae80
Show file tree
Hide file tree
Showing 2 changed files with 108 additions and 24 deletions.
52 changes: 28 additions & 24 deletions src/sandbox/synth.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,6 @@
import click


@click.command(
context_settings={'help_option_names': ['-h', '--help']},
no_args_is_help=True)
@click.argument('file', nargs=1,
required=True,
type=click.Path(exists=False, file_okay=True, dir_okay=False, path_type=Path))
@click.option('-a', '--a4',
default=440,
show_default=True,
help='Tuning frequency.')
@click.option('-b', '--bpm',
default=120,
show_default=True,
help='Beats per minute.')
@click.option('-g', '--prog',
default=0,
show_default=True,
help='MIDI program number.')
@click.option('-p', '--play',
default=False,
is_flag=True,
help='Play generated file.')
def synth(file: Union[str, PathLike], *, a4: int = 440,
bpm: int = 120,
prog: int = 0,
Expand All @@ -59,7 +37,6 @@ def synth(file: Union[str, PathLike], *, a4: int = 440,
track.append(Message('note_off', note=note, velocity=100, time=240))

track.append(MetaMessage('end_of_track'))

midi.save(file.with_suffix('.mid'))

scale = create_edo_scale(12)
Expand All @@ -84,6 +61,33 @@ def synth(file: Union[str, PathLike], *, a4: int = 440,
run(['play', file], check=True)


@click.command(
context_settings={'help_option_names': ['-h', '--help']},
no_args_is_help=True)
@click.argument('file', nargs=1,
required=True,
type=click.Path(exists=False, file_okay=True, dir_okay=False, path_type=Path))
@click.option('-a', '--a4',
default=440,
show_default=True,
help='Tuning frequency.')
@click.option('-b', '--bpm',
default=120,
show_default=True,
help='Beats per minute.')
@click.option('-p', '--prog',
default=0,
show_default=True,
help='MIDI program number.')
@click.option('-y', '--play',
default=False,
is_flag=True,
help='Play generated file.')
def main(file, a4, bpm, prog, play):

synth(file, a4=a4, bpm=bpm, prog=prog, play=play)


if __name__ == '__main__':

synth() # pylint: disable=no-value-for-parameter
main() # pylint: disable=no-value-for-parameter
80 changes: 80 additions & 0 deletions src/sandbox/tuning.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# pylint: disable=import-error
# pylint: disable=fixme

import matplotlib.pyplot as plot
import numpy as np
import numpy.lib.stride_tricks as tricks
import soundfile

from qdft import Chroma
from synth import synth

CP = 440
test = f'test.{CP}.wav'
synth(test, a4=CP)

samples, samplerate = soundfile.read(test)

samples = np.mean(samples, axis=-1) \
if len(np.shape(samples)) > 1 \
else np.asarray(samples)

print(f'samples {len(samples)} {len(samples)/samplerate}s')
length = int(np.ceil(samples.size / samplerate) * samplerate)
samples.resize(length)
print(f'samples {len(samples)} {len(samples)/samplerate}s')

chunks = tricks.sliding_window_view(samples, samplerate)[::samplerate]
chroma = Chroma(samplerate, feature='hz')

chromagram = np.empty((0, chroma.size))

for i, chunk in enumerate(chunks):

if not i:
print('0%')

chromagram = np.vstack((chromagram, chroma.chroma(chunk)))

print(f'{int(100 * (i + 1) / len(chunks))}%')

# TODO chroma.qdft.latencies in the next release
latency = int(np.max(chroma.qdft.periods[0] - chroma.qdft.offsets))
print(f'latency {latency}')

print(f'old shape {chromagram.shape}')
chromagram = chromagram[latency:]
print(f'new shape {chromagram.shape}')

cp0 = chroma.concertpitch
cp1 = np.full(len(chromagram), cp0, float)

r = np.real(chromagram)
f = np.imag(chromagram)

i = np.arange(len(chromagram))
j = np.argmax(r, axis=-1)

for n, m in zip(i, j):

# TODO peak picking
s = np.round(12 * np.log2(f[n, m] / cp1[n-1]))

cp1[n] = (f[n, m] * 2**(s/12)) / (2**(s/6))
cp1[n] = cp1[n-1] if np.isnan(cp1[n]) else cp1[n]

# TODO better estimation precision
stats = np.ceil([
cp1[0],
cp1[-1],
np.mean(cp1),
np.median(cp1)
])

print(f'fist {stats[0]} last {stats[1]} avg {stats[2]} med {stats[3]}')

plot.figure(test)
plot.plot(cp1)
plot.show()

assert stats[-1] == CP

0 comments on commit 2dfae80

Please sign in to comment.