Skip to content

Commit

Permalink
playing chords
Browse files Browse the repository at this point in the history
  • Loading branch information
pavel-zhur committed Jun 26, 2024
1 parent 0b41b32 commit cb7931f
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,11 @@ public async Task<IActionResult> Song(SongModel songModel)
ViewBag.Chords = chords;

var representationSettings = new RepresentationSettings();
var chordsData = chords.Output.AsChords(representationSettings);

if (songModel.Highlight != null)
{
var chordsProgression = _progressionsBuilder.BuildProgression(chords.Output.AsChords(new()).Select(_chordDataParser.GetProgressionData).ToList());
var chordsProgression = _progressionsBuilder.BuildProgression(chordsData.Select(_chordDataParser.GetProgressionData).ToList());
var searchProgression = _inputParser.Parse(songModel.Highlight);
var found = _progressionsSearch.Search(
chordsProgression.Once().ToList(),
Expand All @@ -80,6 +81,16 @@ public async Task<IActionResult> Song(SongModel songModel)

ViewBag.RepresentationSettings = representationSettings;

ViewBag.Parsed = chordsData
.Distinct()
.Select(x => (x, notes: _chordDataParser.GetNotes(x)))
.Where(x => x.notes.HasValue)
.ToDictionary(x => x.x, x => x.notes!.Value.SelectSingle(x => new PlayerModel
{
Bass = x.bass,
Main = x.main,
}));

return View(songModel);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace HarmonyDB.Playground.Web.Models;

public class PlayerModel
{
public required IReadOnlyList<int> Bass { get; init; }

public required IReadOnlyList<int> Main { get; init; }
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@using HarmonyDB.Common.Representations.OneShelf
@using System.Text.Json
@using HarmonyDB.Common.Representations.OneShelf
@using HarmonyDB.Source.Api.Model.V1
@model SongModel

Expand All @@ -9,6 +10,11 @@
Chords chords = ViewBag.Chords;
}

<script src="~/js/Player.js"></script>
@Html.Hidden("parsedChords", JsonSerializer.Serialize(ViewBag.Parsed), null)

<button onclick="playChord()">play</button>

<div>
<h3 class="d-inline-block">@string.Join(", ", chords.Artists) &ndash; @chords.Title</h3>

Check warning on line 19 in HarmonyDB.Playground/HarmonyDB.Playground.Web/Views/Home/Song.cshtml

View workflow job for this annotation

GitHub Actions / build

Possible null reference argument for parameter 'values' in 'string string.Join(string? separator, IEnumerable<string?> values)'.

Check warning on line 19 in HarmonyDB.Playground/HarmonyDB.Playground.Web/Views/Home/Song.cshtml

View workflow job for this annotation

GitHub Actions / build

Possible null reference argument for parameter 'values' in 'string string.Join(string? separator, IEnumerable<string?> values)'.
@Html.ActionLink("\ud83d\udd17Permalink", "Song", "Home", Model, new { target = "_blank" })
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
div.chords-block div.pre-variable-width {
body:has(div.chords-block) {
user-select: none;
}

div.chords-block div.pre-variable-width {
white-space: pre-wrap;
font-family: Tahoma, Roboto, Arial, Helvetica, sans-serif;
}
Expand Down
116 changes: 116 additions & 0 deletions HarmonyDB.Playground/HarmonyDB.Playground.Web/wwwroot/js/player.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
const context = new AudioContext();

// Signal dampening amount
let dampening = 0.99;

// Returns a AudioNode object that will produce a plucking sound
function pluck(frequency) {
// We create a script processor that will enable
// low-level signal sample access
const pluck = context.createScriptProcessor(4096, 0, 1);

// N is the period of our signal in samples
const N = Math.round(context.sampleRate / frequency);

// y is the signal presently
const y = new Float32Array(N);
for (let i = 0; i < N; i++) {
// We fill this with gaussian noise between [-1, 1]
y[i] = Math.random() * 2 - 1;
}

// This callback produces the sound signal
let n = 0;
pluck.onaudioprocess = function (e) {
// We get a reference to the outputBuffer
const output = e.outputBuffer.getChannelData(0);

// We fill the outputBuffer with our generated signal
for (let i = 0; i < e.outputBuffer.length; i++) {
// This averages the current sample with the next one
// Effectively, this is a lowpass filter with a
// frequency exactly half of sampling rate
y[n] = (y[n] + y[(n + 1) % N]) / 2;

// Put the actual sample into the buffer
output[i] = y[n];

// Hasten the signal decay by applying dampening.
y[n] *= dampening;

// Counting constiables to help us read our current
// signal y
n++;
if (n >= N) n = 0;
}
};

// The resulting signal is not as clean as it should be.
// In lower frequencies, aliasing is producing sharp sounding
// noise, making the signal sound like a harpsichord. We
// apply a bandpass centred on our target frequency to remove
// these unwanted noise.
const bandpass = context.createBiquadFilter();
bandpass.type = "bandpass";
bandpass.frequency.value = frequency;
bandpass.Q.value = 1;

// We connect the ScriptProcessorNode to the BiquadFilterNode
pluck.connect(bandpass);

// Our signal would have died down by 2s, so we automatically
// disconnect eventually to prevent leaking memory.
setTimeout(() => {
pluck.disconnect();
}, 2000);
setTimeout(() => {
bandpass.disconnect();
}, 2000);

// The bandpass is last AudioNode in the chain, so we return
// it as the "pluck"
return bandpass;
}

function strum(bass, notes, stagger = 25) {
// Reset dampening to the natural state
dampening = 0.99;

// Connect our strings to the sink
const dst = context.destination;

for (let index = 0; index < bass.length; index++) {
setTimeout(() => {
pluck(getFrequency(bass[index])).connect(dst);
}, stagger * index);
}

for (let index = 0; index < notes.length; index++) {
setTimeout(() => {
pluck(getFrequency(notes[index])).connect(dst);
}, stagger * (index + bass.length));
}
}

function getFrequency(note) {
// Concert A frequency
const A = 110;

return A * Math.pow(2, note / 12);
}

function mute() {
dampening = 0.89;
}

function playChord(bass, notes) {
context.resume().then(strum(bass, notes));
}

document.addEventListener("DOMContentLoaded", (event) => {
$('b.chord[chord-data]').bind('click', x => {
var chordData = x.target.getAttribute('chord-data');
var all = JSON.parse($('#parsedChords')[0].value);
playChord(all[chordData].Bass, all[chordData].Main);
});
});

0 comments on commit cb7931f

Please sign in to comment.