Skip to content

Commit

Permalink
Sound improvement
Browse files Browse the repository at this point in the history
  • Loading branch information
antitim committed Oct 22, 2023
1 parent 417bacd commit cec9a20
Show file tree
Hide file tree
Showing 9 changed files with 139 additions and 62 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
# 2.0.0

- The filter setting has been removed and the filter has also been removed.
- A noise reset method has been added. It is supposed to be used to create the effect of changing the frequency of the radio receiver.
- The ability to add your own AudioNode has been added.
- Added a crackling effect.
- Added pre-amplification to the input signal.
- The noise level was corrected.
- Added sound visualization in the example.

# 1.3.0

- Updated the sound of the noise.
Expand Down
1 change: 0 additions & 1 deletion lib/analog_radio.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
library analog_radio;

export 'src/analog_radio.dart';
export 'src/filter_type.dart';
15 changes: 9 additions & 6 deletions lib/src/analog_radio.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import 'dart:web_audio';

import 'audio_processor.dart';
import 'audio_processor_state.dart';
import 'filter_type.dart';

/// Emulates analog radio
abstract class AnalogRadio {
factory AnalogRadio() {
return AudioProcessor();
factory AnalogRadio({
AudioNode Function(AudioContext context)? setCustomNode,
}) {
return AudioProcessor(setCustomNode: setCustomNode);
}

/// Radio state
Expand All @@ -20,6 +23,9 @@ abstract class AnalogRadio {
/// Adjusts the radio to the [url] stream with the signal strength [signalStrength]. [signalStrength] must be from 0 to 1
void tune(String url, double signalStrength);

/// Turns on the frequency change sound
void tuning();

/// The current volume. From 0 to 1
double volume = 1;

Expand All @@ -29,9 +35,6 @@ abstract class AnalogRadio {
/// Internal signal strength. From 0 to 1
double internalSignalStrength = 1;

/// Applied filter
FilterType filter = FilterType.allpass;

/// The radio is playing
bool get playing;

Expand Down
61 changes: 24 additions & 37 deletions lib/src/audio_processor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import 'dart:web_audio';

import 'audio_player.dart';
import 'audio_processor_state.dart';
import 'filter_type.dart';
import 'analog_radio.dart';
import 'radio_node.dart';

Expand All @@ -12,39 +11,48 @@ class AudioProcessor implements AnalogRadio {
late AudioContext _audioContext;
late AudioPlayer _audioPlayer;
late RadioNode _radioNode;
late GainNode _preGainNode;
late GainNode _gainNode;
late BiquadFilterNode _filterNode;
double _signalStrength = 0;
Timer? _watchingTimer;

/// Speed of signal level change
Duration speedLevelChange;

/// Adding custom audio node
AudioNode Function(AudioContext context)? setCustomNode;

StreamController<AudioProcessorState> _stateStreamCtrl =
StreamController<AudioProcessorState>();

Stream<AudioProcessorState> get state => _stateStreamCtrl.stream;

AudioProcessor({
this.speedLevelChange = const Duration(milliseconds: 10),
this.setCustomNode,
}) {
_audioContext = AudioContext();
_audioPlayer = AudioPlayer();

var source = _audioContext.createMediaElementSource(_audioPlayer.element);
_radioNode = RadioNode(_audioContext);

_filterNode = _audioContext.createBiquadFilter();
_filterNode.type = 'allpass';
_filterNode.frequency?.value = 3500;
_filterNode.Q?.value = 1.7;

_gainNode = _audioContext.createGain();

source.connectNode(_radioNode.node);
_radioNode.node.connectNode(_filterNode);
_filterNode.connectNode(_gainNode);
_gainNode.connectNode(_audioContext.destination!);
_preGainNode = _audioContext.createGain();
_preGainNode.gain?.value = 1.8;

source.connectNode(_preGainNode);
_preGainNode.connectNode(_radioNode.node);
_radioNode.node.connectNode(_gainNode);

if (setCustomNode != null) {
var customNode = setCustomNode!(_audioContext);
_gainNode.connectNode(customNode);
customNode.connectNode(_audioContext.destination!);
} else {
_gainNode.connectNode(_audioContext.destination!);
}

// Playback has started
_audioPlayer.onPlaying.listen((event) => _dispatchCurrentState());
Expand Down Expand Up @@ -95,6 +103,11 @@ class AudioProcessor implements AnalogRadio {
_audioPlayer.play(url);
}

@override
void tuning() {
_radioNode.resetNoise();
}

void _startWatching() {
_watchingTimer = Timer.periodic(speedLevelChange, (_) {
var diff = (playing ? _signalStrength : 0) - _radioNode.signalStrength;
Expand Down Expand Up @@ -132,38 +145,12 @@ class AudioProcessor implements AnalogRadio {
_dispatchCurrentState();
}

@override
set filter(FilterType value) {
switch (value) {
case FilterType.allpass:
_filterNode.type = 'allpass';
break;
case FilterType.bandpass:
_filterNode.type = 'bandpass';
break;
}

_dispatchCurrentState();
}

@override
double get signalStrength => _signalStrength;
@override
double get internalSignalStrength => _radioNode.signalStrength;
@override
double get volume => _gainNode.gain!.value!.toDouble();
@override
FilterType get filter {
switch (_filterNode.type) {
case 'allpass':
return FilterType.allpass;
case 'bandpass':
return FilterType.bandpass;
default:
return FilterType.allpass;
}
}

@override
bool get playing => _audioPlayer.buffered.length > 0;
@override
Expand Down
2 changes: 0 additions & 2 deletions lib/src/filter_type.dart

This file was deleted.

49 changes: 39 additions & 10 deletions lib/src/radio_node.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@ import 'dart:web_audio';
class RadioNode {
ScriptProcessorNode _node;
final Random _random = Random(10);

/// From 0 to 1
double signalStrength = 0;

// For saw wave
int _sawWaveI = 0;
int _sawWave2I = 0;
int _freq = 600;
int _freq2 = 62;
double _sawVolume1 = 0.25;
double _sawVolume2 = 0.1;
double _sawVolume1 = 0.2;
double _sawVolume2 = 0.4;

// For pink noise
double _b0 = 0;
Expand All @@ -23,24 +25,41 @@ class RadioNode {
double _b5 = 0;
double _b6 = 0;

resetNoise() {
_sawWaveI = _random.nextInt(_freq);
_sawWave2I = _random.nextInt(_freq2);
_sawVolume1 = _random.nextDouble() / 5;
_sawVolume2 = _random.nextDouble();

_b0 = _random.nextDouble();
_b1 = _random.nextDouble();
_b2 = _random.nextDouble();
_b3 = _random.nextDouble();
_b4 = _random.nextDouble();
_b5 = _random.nextDouble();
_b6 = _random.nextDouble();
}

RadioNode(AudioContext ctx) : _node = ctx.createScriptProcessor(4096, 2, 2) {
_node.onAudioProcess.listen(onAudioProcessHandler);
}

AudioNode get node => _node;

double get _signalStrengthRelative => pow(signalStrength, 1 / 1.5).toDouble();

double _cutSignal(double signal) {
var maxSignalStrength = (_signalStrengthRelative - 0.5) * 2;
var signalStrengthRelative = pow(signalStrength, 0.6);
var maxSignalStrength = (signalStrengthRelative - 0.5) * 2;
if (signal > maxSignalStrength) signal = maxSignalStrength;

var waveShift = (1 - maxSignalStrength) / 2;

return signal + waveShift;
}

/// https://www.firstpr.com.au/dsp/pink-noise/
double _addPinkNoise(double signal) {
var signalStrengthRelative = pow(signalStrength, 0.4);

var white = _random.nextDouble() * 2 - 1;
_b0 = 0.99886 * _b0 + white * 0.0555179;
_b1 = 0.99332 * _b1 + white * 0.0750759;
Expand All @@ -52,7 +71,18 @@ class RadioNode {
pink *= 0.24;
_b6 = white * 0.115926;

return signal + pink * (1 - _signalStrengthRelative) / 6;
var crackle = 0.0;
var crackleFrom = 0.8; // For 0 signalStrength
var crackleTo = 1.4; // For 1 signalStrength

var pinkNoiseGateLevel =
signalStrength * (crackleTo - crackleFrom) + crackleFrom;

if (pink.abs() > pinkNoiseGateLevel) {
crackle = pink;
}

return signal + pink * (1 - signalStrengthRelative) + crackle;
}

double _addSawWave(double signal) {
Expand All @@ -64,12 +94,11 @@ class RadioNode {
_sawVolume1 = 0;
}

signal = signal +
(_sawWaveI / _freq) * (1 - _signalStrengthRelative) * _sawVolume1;
signal = signal + (_sawWaveI / _freq) * (1 - signalStrength) * _sawVolume1;
if (_sawWaveI > _freq) _sawWaveI = 0;

signal = signal +
(_sawWave2I / _freq2) * (1 - _signalStrengthRelative) * _sawVolume2;
signal =
signal + (_sawWave2I / _freq2) * (1 - signalStrength) * _sawVolume2;
if (_sawWave2I > _freq2) _sawWave2I = 0;

_sawWaveI++;
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: analog_radio
description: Utility for adding analog radio noise to online radio. Uses the WebAudio Api.
homepage: https://github.com/antitim/analog_radio
repository: https://github.com/antitim/analog_radio
version: 1.3.0
version: 2.0.0
environment:
sdk: ">=3.0.0 <4.0.0"
dev_dependencies:
Expand Down
3 changes: 2 additions & 1 deletion web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
</head>
<body>
<h1>Radio Noise</h1>
<canvas id="visual" width="600" height="200"></canvas>
<label>Volume</label>
<input type="range" min="0" max="1" step="0.01" class="volume" value="0"/>
<input type="range" min="-30" max="0" step="1" class="volume" value="-30"/>
<label>Frequency <span class="frequency-value"></span> kHz</label>
<input type="range" step="1" class="freq" />
<h2>Stations</h2>
Expand Down
58 changes: 54 additions & 4 deletions web/main.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import 'dart:html';
import 'dart:math';
import 'dart:typed_data';
import 'dart:web_audio';

import 'package:analog_radio/analog_radio.dart';

Expand All @@ -10,8 +13,13 @@ void main() async {
var $freq = document.querySelector('.freq') as RangeInputElement;
var $freqValue = document.querySelector('.frequency-value') as Element;
var $stations = document.querySelector('.stations') as TextAreaElement;
var $visual = document.getElementById('visual') as CanvasElement;
late AnalyserNode analyser;

var radio = AnalogRadio();
var radio = AnalogRadio(setCustomNode: (context) {
analyser = context.createAnalyser();
return analyser;
});
var stations = Stations();

$freq.setAttribute('min', stations.frequencyMin);
Expand All @@ -22,6 +30,8 @@ void main() async {
var freq = int.tryParse($freq.value!) ?? 0;
var station = stations.getStationByFreq(freq);

radio.tuning();

if (station == null) {
radio.signalStrength = 0;
radio.internalSignalStrength = 0;
Expand All @@ -37,12 +47,12 @@ void main() async {

var volumeHandler = (Event event) {
var val = double.parse((event.target as InputElement).value!);
if (val == 0) {
if (val == -30) {
radio.turnOff();
} else {
radio.turnOn();
}
radio.volume = val;
radio.volume = pow(10, (val / 20)).toDouble();
};

void drawStationsFrequency(List<Station> stations) {
Expand Down Expand Up @@ -70,7 +80,7 @@ void main() async {
drawStationsFrequency(stations.stations);
};

$volume.value = '0';
$volume.value = '-30';

var initialFreq = window.location.hash.replaceAll('#', '');

Expand All @@ -86,4 +96,44 @@ void main() async {
$volume.onInput.listen(volumeHandler);
$freq.onInput.listen(freqHandler);
$stations.onChange.listen(stationsChangeHandler);

// Draw visualization
var WIDTH = 600;
var HEIGHT = 200;
analyser.fftSize = 2048;
var ctx = $visual.getContext('2d') as CanvasRenderingContext2D;
ctx.clearRect(0, 0, WIDTH, HEIGHT);
var bufferLength = analyser.frequencyBinCount;
var dataArray = Uint8List(bufferLength!);

void drawVisual(_) {
analyser.getByteTimeDomainData(dataArray);
ctx.fillStyle = "rgb(33, 33, 33)";
ctx.fillRect(0, 0, WIDTH, HEIGHT);
ctx.lineWidth = 2;
ctx.strokeStyle = "rgb(220, 220, 220)";
ctx.beginPath();
var sliceWidth = WIDTH / bufferLength;
double x = 0;

for (var i = 0; i < bufferLength; i++) {
var v = dataArray[i] / 128.0;
var y = HEIGHT - v * (HEIGHT / 2);

if (i == 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}

x += sliceWidth;
}

ctx.lineTo(WIDTH, HEIGHT / 2);
ctx.stroke();

window.requestAnimationFrame(drawVisual);
}

window.requestAnimationFrame(drawVisual);
}

0 comments on commit cec9a20

Please sign in to comment.